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

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?

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.

1 Like

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
4 Likes

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.

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.

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], " ");

@jamesh any thoughts?

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

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

1 Like

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.

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.

@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”?

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.

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?

I turned up this:

Which looks like it might provide the basic file parsing.

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

1 Like

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.

I’ve also been meaning to respond to this for a while. Thanks for working on this! I’ve read the topic and the branch. Some random thoughts without repeating (too much) what @jamesh, etc have mentioned or any nitpicking (well, one nitpick: running go fmt on all your changed/added files is recommended ;):

  • moving the to shell-support is the perfect first step. We can do everything we need to support a shell, learn a bit in the process and then tease out stuff that could be in other interfaces (eg, allowing a non-shell-support-using snap to launch its own desktop files)
  • Yes, the base declaration for shell-support will need to made more restrictive
  • /var/lib/snapd/desktop/applications/{,*} r, - if we bind mounted /usr/share/applications from the host into the snap, you could also launch non-snaps. Or we allow /var/lib/snapd/hostfs/usr/share/applications and the shell knows to look there
  • depending on what we are going to support for launching (eg desktop actions, we may want to perform additional desktop file checks/sanitizing (nothing specific; just mentioning when it goes up)). Eg, making readExecCommandFromDesktopFile() bullet-proof, etc
  • I don’t think the snapd team would want to call out to glib’s GAppInfo in general. Though, perhaps since this is only userd, calling out to C wouldn’t be terrible. Definitely get buy in from @pedronis or @mvo before changing to that though

Approach looks good and is pretty much what I expected after we last spoke @alan_g. Looking forward to the PR! :slight_smile:

1 Like

Thanks! Timely encouragement as I’m just swinging back to this after dealing with a couple of issues that came up whilst experimenting. (The one most relevant here is https://github.com/snapcore/snapd/pull/7409)

An alternative that did occur to me (because io.snapcraft.Launcher.OpenURL already execs xdg-open) is writing a C utility to parse the file and launch the app. But separating it out brings complexity of its own.

1 Like

Here you go: https://github.com/snapcore/snapd/pull/7490

1 Like

Thanks! I’ve added this to my review queue and tracking it in trello.

1 Like