How can a confined snap run other snaps (or applications)?


#1

Last year I kicked off a discussion How to confine a desktop shell one thread of which was how to launch applications (including other snaps). At the time the answer was “it isn’t possible now, but maybe something like userd will support it in the future”.

I see that work has been progressing on userd: A “user session agent” for snapd and I see from How to autostart a snap of a desktop application? that starting applications has been discussed, but that isn’t quite the scenario I (and a few others, e.g Executing a snap from within a snap package) am interested in.

In the hope of a direct and (even more hopefully) positive answer I’m splitting this thread into a new topic:

How can a confined snap run other snaps (or applications)?


#2

Note that the user session agent in its current form is not really relevant: it is intended to perform actions within the user session on behalf of snapd. What you are after is “perform actions within the user session on behalf of a confined application”, which is still very much userd’s purview.


#3

@jamesh you imply there’s still no current mechanism? Is there any guidance on what would acceptable?


#4

I’m just saying that the user session agent is not a solution to the problem you’ve outlined. To my knowledge, nothing has been done since the previous conversations, so the first steps would be to outline what the requirements are. Without knowing that, it is hard to make any informed comment about security.

The base requirement is to launch an application that is not contained within the calling snap. The most obvious way to handle this would be a D-Bus API exposed on the session bus (possibly part of userd), where access can be both gated via an interface and allow more fine grained checking by the helper service. Some questions to answer include:

  • Are we launching executables or desktop files here? Desktop files would have the benefit of providing additional metadata, but also mean that this is explicitly an API for launching graphical applications.
  • Do you want to support opening documents with the launched application?
    • What if the target application can’t read/write the given file? (the document portal could be part of the solution here).
  • Can we assume any environment variables necessary to launch desktop apps (e.g. DISPLAY, WAYLAND_DISPLAY, etc) are already in the host system’s user environment?

The next obvious question is: how will a confined snap know what applications are available to launch? For a desktop shell, this almost certainly means access to desktop files, since you’d want to know names of applications too.

  • If we limit this to launching snap packaged applications, this could be done by allowing read access to /var/lib/snapd/desktop/applications, which contains all the desktop files for installed desktop snaps. This directory is already mapped into the sandbox, so access is just one AppArmor rule away.

  • If you want to launch arbitrary host system apps, things are a bit more complicated. On my system, I have application desktop files installed in:

    • ~/.local/share/applications
    • /usr/share/applications
    • /usr/share/ubuntu/applications
    • /usr/share/ubuntu-wayland/applications
    • /usr/local/share/applications

    And there could be more depending on what $XDG_DATA_DIRS is set to. If you want access to these applications, it would probably need to be proxied via a D-Bus service.

    Complicating matters further, most of the desktop files in /usr/share/applications do not contain translations, instead having an X-Ubuntu-Gettext-Domain key, delegating the translations to one of the message catalogues in /usr/share/locale. So any proxy would need to retrieve the translations corresponding to the user’s locale, rather than just return the desktop file content verbatim.

Also, a desktop shell will want to display icons for any applications it makes available to launch.

  • For snap confined applications, the Icon= line in the desktop file is usually an absolute path to an image file within the snap. There are no current restrictions on where in the snap this image file could be located, so a targeted AppArmor rule would be difficult.
    • Another thing to consider is systems where snaps are mounted under /var/lib/snapd/snap. On these systems, the icon will probably have a /var/lib/snapd/snap path even though snaps appear at /snap within the sandbox.
    • PR #6767 might help a bit here by allowing snaps to install their icons to /var/lib/snapd/desktop/icons, but this is something individual snaps would need to migrate to.
  • For non-snap applications, icons will likely be spread out over /usr/share/icons, with the rules for selection based on the desktop’s chosen icon theme and the intended display size. I’m not sure what a reasonable D-Bus interface to support this might look like.

#5

Thanks for asking all the right questions. To an extend I’ve been exploring this problem space by seeing how far I can get, fixing the next pain point and iterating. My instinct at this point is to go for the simplest set of requirements that might possibly be usable and reassess after that.

A simple, confined desktop

A confined graphical shell that launches other snaps based on /var/lib/snapd/desktop/applications provides a lot of flexibility. (In a classic environment it would even allow “classic” snaps to be run and arbitrary execution from there.)

As you say, reading this directory should just be an AppAmor rule. But that’s just the start.

Executables or .desktop files

Sticking to desktop files avoids a slippery slope that leads to something like execvpe() and passing file descriptors.

It just leaves the questions of how to pass any % arguments used in Exec and what interprets the .desktop contents. (I see xdg-open in the Launcher source, but, to the Internet’s surprise xdg-open doesn’t understand .desktop files - a shame as otherwise they could be passed to OpenURL.)

I guess a first iteration could simply the Exec line and exec that. (Which is what I do in egmde currently.)

Icons

Currently, as you observer, icon’s won’t work (and are already broken on some distros):

$ grep Icon= /var/lib/snapd/desktop/applications/*.desktop 
/var/lib/snapd/desktop/applications/cevelop_cevelop.desktop:Icon=/snap/cevelop/x1/icon.xpm
/var/lib/snapd/desktop/applications/classic-snap-analyzer_classic-snap-analyzer.desktop:Icon=/snap/classic-snap-analyzer/5/meta/gui/classic-snap-analyzer.png
/var/lib/snapd/desktop/applications/clion_clion.desktop:Icon=/snap/clion/81/bin/clion.png
/var/lib/snapd/desktop/applications/gnome-calculator_gnome-calculator.desktop:Icon=/snap/gnome-calculator/406/meta/gui/org.gnome.Calculator.svg
/var/lib/snapd/desktop/applications/gnome-characters_gnome-characters.desktop:Icon=/snap/gnome-characters/296/meta/gui/org.gnome.Characters.svg
/var/lib/snapd/desktop/applications/gnome-logs_gnome-logs.desktop:Icon=/snap/gnome-logs/61/meta/gui/org.gnome.Logs.svg
/var/lib/snapd/desktop/applications/gnome-system-monitor_gnome-system-monitor.desktop:Icon=/snap/gnome-system-monitor/100/meta/gui/org.gnome.SystemMonitor.svg
/var/lib/snapd/desktop/applications/inkscape_inkscape.desktop:Icon=/snap/inkscape/4693/share/inkscape/branding/inkscape.svg
/var/lib/snapd/desktop/applications/multipass_multipass-gui.desktop:Icon=/snap/multipass/1002/meta/gui/multipass-gui.svg
/var/lib/snapd/desktop/applications/vlc_vlc.desktop:Icon=/snap/vlc/1049/usr/share/icons/hicolor/256x256/apps/vlc.png

I’m going to declare that “out of scope” for the current discussion. if a standard location materializes then it becomes easy.

Environment variables to launch desktop apps

The only successfully confined desktop environment I know of is egmde-confined-desktop. In this, the environment variables are not available in the host user environment (if any).

Variable Source Comment
XDG_RUNTIME_DIR Set by snapd on classic, different to the user session
WAYLAND_DISPLAY Set by egmde when launching an app
DISPLAY This snap doesn’t enable X11 apps Would set by egmde when launching an app
XDG_RUNTIME_DIR and WAYLAND_DISPLAY

We need a way to map the file $XDG_RUNTIME_DIR/$WAYLAND_DISPLAY in requesting context to $XDG_RUNTIME_DIR/$WAYLAND_DISPLAY in the application context.

There’s discussion elsewhere (Wayland interface, $XDG_RUNTIME_DIR and connecting clients to server) about managing XDG_RUNTIME_DIR. (WAYLAND_DISPLAY ought to be considered there too, but is assumed to be unset). If we assume that both server and client snaps do the “dance” described there, then it should work.

DISPLAY

I’m content to leave support for X11 based applications for a later iteration.

Opening files

I’m tempted to say “not a problem”. Any application that can be passed a filename should cope with the filename being invalid or the file being inaccessible. But if we don’t pass filenames in the first iteration we can postpone further consideration of this.

A first iteration

With the limitations discussed above, it seems much could be achieved by adding an OpenDesktop method to io.snapcraft.Launcher that accepts the name of the .desktop file, extracts and sanitizes the Exec line and execs the resulting command in much the same manner as OpenURL execs xdg-open.

Do you see anything I am missing?


#6

Environment variables

On the subject of setting WAYLAND_DISPLAY / DISPLAY, I think you’d want a confined display server to create its sockets in the same place as an unconfined display server would. If you place them elsewhere, then the AppArmor confinement of other applications will refuse to let them connect.

For the x11 interface slot AppArmor rules, the display server is allowed to listen on abstract sockets like /tmp/.X11-unix/X0. For the wayland interface, the slot AppArmor rules let you create /run/user/[0-9]*/wayland-[0-9]*. Making use of the standard paths seems more promising than trying to map in non-standard XDG_RUNTIME_DIR values.

As for the environment variables themselves, I think we probably don’t want a per-launch method of setting them: every app in the desktop should share the same value, including for processes not launched directly by the desktop shell. So the model should probably be “export these environment variables to the user session”. That is, a safe version of systemctl --user set-environment.

User session

If you are trying to run a confined desktop, I think you will still need to rely on a few components outside of confinement. The big one is the D-Bus session bus. While you can launch a bus within your snap, it won’t be able to perform AppArmor checks and won’t match any of the rules on the peer side either. Most importantly, a confined session bus won’t be able to launch an unconfined snap userd.

So for now, I think this kind of project needs to be limited to the kind of environment gdm or would launch your desktop in: that is, with a systemd user session taking care of starting the dbus-daemon, configuring XDG_RUNTIME_DIR, etc.

At present, this infrastructure is not available on Ubuntu Core systems. While it is possible to launch a systemd user session, the unit files necessary to create a session bus are missing from both core and core18 snaps. It would also need some way to launch the user session on device boot. So for now, it seems like any solution is going to be classic-only.

Once something is working with classic however, we’d be in a position to know what features are needed on core.


#7

I’ve started experimenting and, I think, have a proof of concept that establishes that things could work as discussed.

snapd

I’ve hacked snapd’s userd locally so that io.snapcraft.Launcher.OpenURL has an additional “desktoplaunch” scheme. (Probably not the way to go for production, but minimized changes.)

This “desktoplaunch” scheme doesn’t (yet) do anything clever, just execs the command that follows.

egmde-confined-desktop

I’ve also locally added the desktop interface to egmde-confined-desktop so that it can use io.snapcraft.Launcher.OpenURL.

The proof

Now with egmde-confined-desktop and mir-kiosk-kodi installed I can launch a (confined) shell in egmde and launch the Kodi snap with:

dbus-send --session  --print-reply /io/snapcraft/Launcher --dest=io.snapcraft.Launcher \
io.snapcraft.Launcher.OpenURL string:desktoplaunch:mir-kiosk-kodi

#8

Work in progress

OK, so this isn’t production code, but I’m sharing it for feedback on how to proceed.

I’ve a hacked version of snapd that adds io.snapcraft.Launcher.OpenDesktopEntry and access to /var/lib/snapd/desktop/applications to the desktop interface.

I’ve also branched egmde to use this and created a version of the egmde-confined-desktop snap that I’ve pushed to --channel=edge/open-desktop-entry.

With all these installed it is possible to launch wayland based snaps from the confined desktop.

Known limitations

  1. None of this works on Ubuntu Core as there’s no userd.
  2. Although the desktop interface gives access to /usr/share/applications this is bound to the base snap (core18 in this example).
  3. We need to pass WAYLAND_DISPLAY: Because we don’t pass WAYLAND_DISPLAY the client will connect to whichever Wayland server started first.
  4. We need to pass any exec variables.

Feedback please

  1. Is this a sensible interface to use? Or can something better be suggested?
  2. Should we look for a way to launch applications with desktop files in /usr/share/applications?
  3. Any ideas for supporting Ubuntu Core?

Play along instructions

If you want to experiment, here are the steps to reproduce what I’ve done so far.

Start with an X11 based desktop. (A Wayland based desktop will confuse things - that’s one of the issues.)

Checkout the https://github.com/MirServer/snapd/tree/open-desktop-entry branch and run the following script. (This replaces the affected binaries and waits before restoring the system.)

#!/usr/bin/env bash

sudo rm /tmp/snapd /tmp/snap-seccomp /tmp/snap || true
go build -o /tmp/snap github.com/snapcore/snapd/cmd/snap
go build -o /tmp/snapd github.com/snapcore/snapd/cmd/snapd
go build -o /tmp/snap-seccomp github.com/snapcore/snapd/cmd/snap-seccomp

sudo systemctl stop snapd snapd.service snapd.socket
sudo systemctl set-environment SNAP_REEXEC=0 SNAPD_DEBUG=1 SNAPD_DEBUG_HTTP=3
killall /usr/bin/snap || true
sudo mount --bind /tmp/snap /usr/bin/snap
sudo mount --bind /tmp/snapd /usr/lib/snapd/snapd
sudo mount --bind /tmp/snap-seccomp /usr/lib/snapd/snap-seccomp
sudo systemctl start snapd snapd.service snapd.socket

read -p "Press enter to revert"

sudo systemctl stop snapd snapd.service snapd.socket || true
killall /usr/bin/snap || true
sudo umount /usr/lib/snapd/snapd
sudo umount /usr/lib/snapd/snap-seccomp
sudo systemctl unset-environment SNAP_REEXEC SNAPD_DEBUG SNAPD_DEBUG_HTTP
sudo systemctl start snapd snapd.service snapd.socket
sudo umount /usr/bin/snap

While the script is waiting install the test version of egmde-confined-desktop and run it, I suggest also installing mir-kiosk-kodi as a test application:

snap install --channel=edge/open-desktop-entry egmde-confined-desktop
/snap/egmde-confined-desktop/current/bin/setup.sh
snap install mir-kiosk-kodi
/snap/mir-kiosk-kodi/current/bin/kodi-connect.sh
snap connect mir-kiosk-kodi:wayland egmde-confined-desktop:wayland
egmde-confined-desktop

Egmde will appear in a “Mir-on-X” window and can launch the kodi snap via userd. (I’ve noticed it doesn’t always work on the first attempt - I think there’s a race in starting userd as once that starts subsequent attempts work.)

It is also possible to select a confined egmde session from the greeter.


#9

This is looking promising to me :slight_smile:

Does this support the extra actions defined in .desktop files?

I don’t think we want this in the desktop interface, do we? Most (if not all) GUI applications will have the desktop interface - we don’t want them to be able to open other apps, they should use xdg-open and URLs for that.

On that note, I wonder how much different is opening URLs to what you propose. Back in the phone days we had a application:///… URL schema that would launch the app that was called out. Maybe that would be a way for non-shells to still be able to request an external app? Or maybe we should just stick to URLs and have apps register the URL patterns they want to handle, and give the user a choice.


#10

Not currently. The processing of the .desktop file is very primitive:

    if strings.HasPrefix(line, "Exec=") {
      launch = strings.TrimPrefix(line, "Exec=")
      break;
    }
  }

  // This is very hacky parsing and doesn't cover a lot of cases
  command := strings.Split(strings.SplitN(launch, "%", 2)[0], " ");

#11

@jamesh any thoughts?


#12

on core all apps would obviously come from snap packages so you’d simply look in /var/lib/snapd/desktop/applications/ instead of /usr/share/applications


#13

That’s true, but AIUI there’s no user session and therefore no userd to launch anything.


#14

Sorry for the delay in getting back to you. I’ve been busy at GUADEC for the last few days. Here are a few thoughts:

  1. I would suggest passing just the desktop ID (i.e. foo.desktop rather than /path/to/foo.desktop) in OpenDesktopEntry. We can then search for the desktop file according to the precedence rules in the spec, then check that it originates from a directory controlled by snapd. Ideally we’d use a full implementation of the desktop entry spec, including disabled desktop files.

  2. Are the extra environment variables you’re setting for the command you spawn actually necessary? If so, it probably makes more sense to export them to the session (i.e. systemctl --user set-environment foo=bar). If the display server is confined, then we probably want some way to invoke the appropriate systemctl commands on its behalf (with validation, and limiting it to suitably privileged snaps). Of course, this results in a problematic ordering:

    • display-server starts
    • display-server invokes userd API to update environment
    • userd starts with the current systemd environment, and makes set-environment calls.
    • display-server invokes OpenDesktopEntry, but userd is still using the environment from when it was started.

    This could be worked around by remembering the new environment variables if set-environment is successful.

  3. For Ubuntu Core, I think we’d need some additional support from the boot base snap (systemd jobs for D-Bus session bus), and some way to install a login manager type app (i.e. something that can talk to logind, probably access PAM, invoke the session as the appropriate user ID).

I think the general idea is sound. There might also be some things to learn from ubuntu-app-launch. That tool is a bit over-engineered, but has some good ideas such as launching apps via transient systemd units.


#15

Are you aware of a suitable desktop entry spec implementation? I see some parsing in usersession/autostart/autostart.go that includes disabled desktop files, but it doesn’t look like a “full implementation” with desktop actions and the like. (I’m not familiar enough with Go and its resources to know if I’ve looked in the right places.)

Not in my mir-kiosk-kodi example. But some toolkits will default to X11 and need encouragement to use Wayland instead. I guess my habit of running multiple X11, Mir and Wayland servers in a user session biases me towards passing these variables to the process I’m starting. OTOH I don’t think it unreasonable to support running a confined desktop as a window on a traditional desktop.

My intention was to allow the shell to pass a list of environment variables (and not to leave them hard coded in userd). Would that address your concerns? Or would you still want to set them in the session?

I guess that’s the main thing for me to look into next.


#16

@jamesh, @Saviq pointed out on IRC that desktop is probably the wrong interface to use for this.

I don’t see any existing interfaces that seem appropriate, so any thoughts on “app-launch” or “snap-app-launch”?


#17

I’m not aware of a full implementation. A quick search turned up github.com/rkoesters/xdg/desktop, but the launch implementation reads:

I’d hope for something with a similar feature set to glib’s GAppInfo code. It would be awesome if we could use that code directly, but it seems unlikely that would be accepted.

I’m not saying that you shouldn’t set environment variables. I’m just questioning whether it makes sense to set them on a per-launch basis rather than providing a way to set them in the systemd environment.

When we get user daemons, it is quite possible that you’ll have processes launched outside of this OpenDesktopEntry API. If the environment variables are necessary, they’re probably needed there too.

Separating it out from desktop is a good idea. As far as naming goes, I think it depends on how the functionality is bundled together. If we include a “set this variable in the environment” API, then something like desktop-shell-support might make sense.


#18

I think it is important to be able to passing environment variables on launch. For example, it is a part of our recommended “graphical snap” development process to host mir-kiosk on a traditional desktop. I think we’d want to have the same option for a confined desktop session. This is a case where we don’t want to change the environment for the session.

But, as you observe, it is obviously also important for some scenarios to be able to set the session environment so that it is available outside of OpenDesktopEntry. I’m simply not sure how that should be integrated, maybe review that once we have experience of user daemons?

Both “opening” desktop files and setting session environment variables seem like something that could have uses beyond desktop shells?


#19

I turned up this:

Which looks like it might provide the basic file parsing.

How about dropping “desktop” and making simply “shell-support”?


#20

Another iteration

I’ve addressed some of the discussion above.

My hacked version of snapd now adds io.snapcraft.Launcher.OpenDesktopEntry, io.snapcraft.Launcher.OpenDesktopEntryEnv access to /var/lib/snapd/desktop/applications to a new shell-support interface.

As currently coded, the shell-support interface auto-connects. I’d expect it be be flagged for manual review for uploads to the store.

I’ve also updated the branched egmde to use this and created a version of the egmde-confined-desktop snap. This can no longer be pushed to the snap store (because “unknown interface ‘shell-support’”).

With all these installed it is possible to launch Wayland based snaps from the confined desktop.

Current status

The main technical features missing here are parsing the .desktop file correctly, adding support for exec variables and desktop actions defined in the .desktop file.

Known limitations

  1. None of this works on Ubuntu Core as there’s no userd.

Play along instructions

If you want to experiment, here are the steps to reproduce what I’ve done so far.

Start with an X11 based desktop. (A Wayland based desktop will confuse things.)

Checkout the https://github.com/MirServer/snapd/tree/open-desktop-entry branch and run the script listed above. (This replaces the affected binaries and waits for <enter> before restoring the system.)

Install mir-kiosk-kodi as a test application:

snap install mir-kiosk-kodi
/snap/mir-kiosk-kodi/current/bin/kodi-connect.sh

Checkout and build the test version of the egmde-confined-desktop snap (this picks up the corresponding egmde branch), install and run it:

git clone https://github.com/MirServer/egmde-confined-desktop.git
cd egmde-confined-desktop
git checkout  open-desktop-entry
snapcraft && snap install --dangerous  *.snap
/snap/egmde-confined-desktop/current/bin/setup.sh
egmde-confined-desktop

Egmde will appear in a “Mir-on-X” window and can launch the kodi snap via userd.