I’d like to propose some changes to the way we handle XDG_RUNTIME_DIR is handled by snapd. The current setup is this:
-
snap run
setsXDG_RUNTIME_DIR=/run/user/$uid/snap.$SNAP_INSTANCE_NAME
in the snap’s environment. - base AppArmor template grants snaps full access to this directory.
- some interfaces grant access to some files in the real
$XDG_RUNTIME_DIR
.
This ends up causing a few problems that need to be papered over by desktop-helpers scripts or similar:
- Nothing creates the private $XDG_RUNTIME_DIR. This violates the fd.o base directory spec, and has caused problems for a number of applications when not using desktop helpers.
- Various libraries search for sockets in
$XDG_RUNTIME_DIR
, for instance Pulse Audio and Wayland.
While desktop-helpers have helped paper over these issues, it’s not so obvious what to do when the server side implementations are also provided by snaps. For example, just as the Wayland client libraries will look for the wayland-0
socket in $XDG_RUNTIME_DIR
, the server side will attempt to create the socket in $XDG_RUNTIME_DIR
.
So if I have a wayland-server
snap, it will end up creating its socket as /run/user/$uid/snap.wayland-server/wayland-0
. Here, I’ve got two options to make the socket available outside the snap:
- write snap specific patches to the server to have it ignore
$XDG_RUNTIME_DIR
, and create its socket in/run/user/$uid
. - write yet more helper scripts. In this case, perhaps fork a background process that waits for the socket to be created, and then hard links it into
/run/user/$uid
?
I think we could do better: solve the problem of sharing sockets between different snaps, and remove the need for some of the desktop-launch changes at the same time. I think most of these would be compatible with most existing desktop-launch scripts too.
Proposal
I think the original idea of giving each snap its own private $XDG_RUNTIME_DIR
is a good idea, since it means we don’t need to restrict what the snap can put in that directory. I think we can preserve that while having the private directory appear at the standard location, similar to how snaps see their private temporary directory at the standard location.
While working adding xdg-desktop-portal support to snapd, I implemented a “user mounts” in snap-confine
/snap-update-ns
in order to mount the per-user document portal into a snap’s mount namespace. It hasn’t been used for anything else since, but I think it could help us here.
When we’re setting up the snap’s mount namespace we’d now do the following:
- in
github.com/snapcore/snapd/snap/snapenv
, setXDG_RUNTIME_DIR=/run/user/$uid
. - Ensure that
/var/lib/snapd/hostfs/run/user/$uid/snap.${SNAP_INSTANCE}
exists. Extend the existingAddModeHint
functionality to ensure that the directory plus parents is created with the right ownership as well as permissions. - Add a user mount of
/var/lib/snapd/hostfs/run/user/$uid/snap.${SNAP_INSTANCE}
to/run/user/$uid
.
Now we’ve still got a private XDG_RUNTIME_DIR
that will be cleaned up with the user’s primary XDG_RUNTIME_DIR
, but still appears at the standard location from the snap’s point of view.
Of course, at this point the snap cannot access anything from the real XDG_RUNTIME_DIR
. This includes:
- D-Bus session bus at
$XDG_RUNTIME_DIR/bus
- X11 auth cookie file, which may be at
$XDG_RUNTIME_DIR/gdm/Xauthority
or possibly other paths. - Wayland sockets that are usually at
$XDG_RUNTIME_DIR/wayland-*
- Pulse Audio socket at
$XDG_RUNTIME_DIR/pulse/native
- dconf update coordination in
$XDG_RUNTIME_DIR/dconf/user
, used the gsettings dconf backend.
The answer to most of these is more user mounts. In the case of data in XDG_RUNTIME_DIR
subdirectories, this is pretty simple:
- The
x11
plug should add a user mount from/var/lib/snapd/hostfs/run/user/$uid/gdm
to/run/user/$uid/gdm
- The
pulseaudio
,audio-playback
, andaudio-record
plugs should add a user mount from/var/lib/snapd/hostfs/run/user/$uid/pulse
to/run/user/$uid/pulse
. There should be some form of de-duplication for snaps that connect more than one of these interfaces. - The
gsettings
plug should mount/var/lib/snapd/hostfs/run/user/$uid/dconf
to/run/user/$uid/dconf
.
(note that we’re mounting via /var/lib/snapd/hostfs
because the host system /run/user/$uid
has been shadowed by earlier mounts).
For cases where sockets (or other non-directory files) are created directly within XDG_RUNTIME_DIR
(e.g. D-Bus and Wayland), we can’t do simple directory mounts. There are two choices:
- Touch an empty file in the private
XDG_RUNTIME_DIR
, and then bind mount the single file over the top of it. - Hard link the file into the private
XDG_RUNTIME_DIR
directly.
Option (2) seems somewhat simpler to me, but perhaps has some downsides I haven’t thought of. Absent appropriate AppArmor protections, both would allow nuisance attacks with chmod, for instance.
In either case, it would require snap-update-ns
updates to support this (especially the globs for e.g. the wayland-*
sockets).
Services provided by snaps
Once we’re in a position where snapd is constructing the snap’s private XDG_RUNTIME_DIR
, it suddenly becomes significantly easier to handle the case of services provided by snaps rather than the host system.
Imagine that we have a version of the pulseaudio
snap that runs the daemon as a user service. From the point of view of the host system mount namespace, the socket will be located at /run/user/$uid/snap.pulseaudio/pulse/native
. If a client snap connects to pulseaudio:audio-playback
instead of the implicit system:audio-playback
slot, we can simply have the interface generate a mount from /var/lib/snapd/hostfs/run/user/$uid/snap.pulseaudio/pulse
instead, which we can infer from the snap instance name of the slot.
Security Concerns
AppArmor permissions
If we change the base template to allow read/write access to /run/user/$uid
with the expectation that a private version will be mounted over the top, we need to be careful about interfaces that add content to the directory. In particular, interfaces will want to explicitly deny write access to those directories.
What about sudo
(or if XDG_RUNTIME_DIR is not set)?
When commands are run under sudo
, no login session is started for the user so systemd does not create the user’s XDG_RUNTIME_DIR
. I don’t think we can simply decide that absence of the directory should short circuit all of this handling though: it is possible that a login session for the user will be started after the snap has started.
If the snap’s AppArmor profile grants read/write access to the /run/user/$uid
path (as it would have to under this proposal), then suddenly the snap has access to everything in the user’s real XDG_RUNTIME_DIR
.
So I think we need to always create the mount, even if it doesn’t previously exist. We’d also need to be careful about what happens to our private directory when systemd starts or stops the user-runtime-dir@.service
service.
Backward compatibility
Looking at the desktop-launch
script created by the various desktop extensions, I see:
- It only sets
PULSE_SERVER
if$XDG_RUNTIME_DIR/../pulse/native
exists. With this change, that code path would not run and libpulse would look for the socket in the regular location (which now works). - Similarly, the Wayland support only tries to symlink the socket if
$XDG_RUNTIME_DIR/../$WAYLAND_DISPLAY
exists. That also becomes a no-op now. - The dconf fixup only runs if
$XDG_RUNTIME_DIR/../dconf/user
exists, so that’s also a no-op.
These all match the behaviour of the old snapcraft-desktop-helpers
scripts, so I suspect the vast majority of snaps will jut work with the change.
I think this about covers it. How does this proposal sound to everyone else?