Electron snaps' tray icons are dead: AppArmor blocks Chromium's StatusNotifierItem object path

Summary

Recent Electron releases register their tray item on Chromium’s D-Bus object path /org/chromium/StatusNotifierItem/<N> instead of /StatusNotifierItem. The desktop-legacy and unity7 interfaces only permit /StatusNotifierItem and /org/ayatana/NotificationItem/*, so the host’s tray implementation is denied when it reads the item’s properties.

The item still registers with org.kde.StatusNotifierWatcher, so the tray reserves a slot for it. The result is an empty, unclickable gap where the icon should be — no icon, no context menu, no activation.

This affects every strictly confined Electron snap, not just the two below.

Environment

  • snapd 2.76.1 (rev 27591), series 16
  • KDE Plasma 5.27.12, X11 session, org.kde.StatusNotifierWatcher served by kded5
  • signal-desktop 8.21.0 (rev 927, snapcrafters)
  • slack 4.51.180 (rev 254)

Both apps regressed simultaneously after an auto-refresh; both are Electron.

Reproducer

Launch a recent Electron snap with a tray icon, then query its item:

$ qdbus org.kde.StatusNotifierWatcher /StatusNotifierWatcher \
        org.kde.StatusNotifierWatcher.RegisteredStatusNotifierItems
...
:1.147/org/chromium/StatusNotifierItem/1

$ dbus-send --session --print-reply --dest=:1.147 \
    /org/chromium/StatusNotifierItem/1 \
    org.freedesktop.DBus.Properties.GetAll string:"org.kde.StatusNotifierItem"

Error org.freedesktop.DBus.Error.AccessDenied: An AppArmor policy prevents this
sender from sending this message to this recipient; type="method_call",
sender=":1.152" (uid=1000 pid=7810 comm="dbus-send ...") label="unconfined"
interface="org.freedesktop.DBus.Properties" member="GetAll"
destination=":1.147" (uid=1000 pid=6954
comm="/snap/signal-desktop/927/opt/Signal/signal-desktop"
label="snap.signal-desktop.signal-desktop (enforce)")

Cause

The generated profile permits only these object paths:

path=/{StatusNotifierItem,org/ayatana/NotificationItem/*}
path=/{StatusNotifierItem/menu,org/ayatana/NotificationItem/*/Menu}
path=/{StatusNotifierItem,StatusNotifierItem/menu,org/ayatana/NotificationItem/**}

/org/chromium/StatusNotifierItem/* is absent, and Chromium additionally serves its DBusMenu under that same prefix.

snapd is not the component that changed. The rule strings are identical in both the previous and current snapd revisions:

$ strings /snap/snapd/25935/usr/lib/snapd/snapd | grep -oE "org/ayatana/NotificationItem[^\"']*|/\{?StatusNotifierItem[^\"']*" | sort -u
$ strings /snap/snapd/27591/usr/lib/snapd/snapd | grep -oE "org/ayatana/NotificationItem[^\"']*|/\{?StatusNotifierItem[^\"']*" | sort -u
# identical output

The change is on the Electron side — see https://github.com/electron/electron/issues/40936 for Electron delegating tray handling to Chromium’s implementation, which owns the /org/chromium/... path.

Note that two distinct object trees need covering. Chromium exports the menu on its own tree rather than below the item’s path — the item’s Menu property reads:

$ dbus-send --session --print-reply=literal --dest=:1.198 \
    /org/chromium/StatusNotifierItem/1 \
    org.freedesktop.DBus.Properties.Get \
    string:"org.kde.StatusNotifierItem" string:"Menu"
variant /org/chromium/DbusMenu/1

so com.canonical.dbusmenu.GetLayout on /org/chromium/DbusMenu/1 is denied separately. Permitting only the item path yields a visible icon with a non-functional context menu.

Suggested fix

Extend the object paths in the desktop-legacy and unity7 interfaces to cover both Chromium trees, e.g.:

path=/{StatusNotifierItem,org/ayatana/NotificationItem/*,org/chromium/StatusNotifierItem/*}
path=/{StatusNotifierItem/menu,org/ayatana/NotificationItem/*/Menu,org/chromium/DbusMenu/*}

Verified workaround

Tested on the environment above: icon renders and the context menu works, with confinement otherwise intact. Drop the following into /etc/apparmor.d/abstractions/dbus-session-strict.d/local-chromium-sni and reload the affected profiles with apparmor_parser -r. That abstraction is included inside every snap profile’s profile {} block, and the .d directory is a supported extension point, so the addition survives both snapd profile regeneration and apparmor package upgrades.

dbus (send)
    bus=session
    path=/org/chromium/StatusNotifierItem/*
    interface=org.kde.StatusNotifierItem
    member="New{AttentionIcon,Icon,IconThemePath,OverlayIcon,Status,Title,ToolTip}"
    peer=(label="{plasmashell,unconfined}"),

dbus (receive)
    bus=session
    path=/org/chromium/StatusNotifierItem/*
    interface=org.kde.StatusNotifierItem
    member={Activate,ContextMenu,Scroll,SecondaryActivate,ProvideXdgActivationToken,XAyatanaSecondaryActivate}
    peer=(label="{plasmashell,unconfined}"),

dbus (send)
    bus=session
    path=/org/chromium/DbusMenu/*
    interface=com.canonical.dbusmenu
    member="{LayoutUpdated,ItemsPropertiesUpdated}"
    peer=(label="{plasmashell,unconfined}"),

dbus (send)
    bus=session
    path=/org/chromium/StatusNotifierItem/**
    interface=org.freedesktop.DBus.Properties
    member=PropertiesChanged
    peer=(label="{plasmashell,unconfined}"),

dbus (receive)
    bus=session
    path=/org/chromium/DbusMenu/**
    interface={org.freedesktop.DBus.Properties,com.canonical.dbusmenu,org.freedesktop.DBus.Introspectable}
    member={Get*,AboutTo*,Event*,Introspect}
    peer=(label="{plasmashell,unconfined}"),

dbus (receive)
    bus=session
    path=/org/chromium/StatusNotifierItem/**
    interface={org.freedesktop.DBus.Properties,com.canonical.dbusmenu,org.freedesktop.DBus.Introspectable}
    member={Get*,AboutTo*,Event*,Introspect}
    peer=(label="{plasmashell,unconfined}"),

The snap-tuning hook is not usable for this: #include if exists "/var/lib/snapd/apparmor/snap-tuning" sits outside the profile {} block, so it accepts tunables but not permission rules. Without the abstraction trick the only remaining option is devmode, which disables confinement entirely.

Could you suggest this patch in snapd? This will be easily accepted IMO