Classic confinement request: communitheme-set-default

@niemeyer - what do you think?

@jdstrand @didrocks Is this a snap getting classic confinement to configure the system for a third snap? That requires some consideration indeed.

What are the actual operations that require the classic support? update-alternatives makes it sound like a command is being changed, which isn’t generally required by a theme.

Can we have some more details about the operation?

1 Like

@niemeyer: This snap configure the arch:all communitheme snap. We need to have communitheme theme as arch: all for multiple reasons:

So, basically, we can’t ship the “dconf” binary for resetting in communitheme snap itself, and we need to a way to access /etc/alternatives symlinks. Indeed, as detailed on planet ubuntu, the gdm theme selection is selected via update-alternatives, more info on https://didrocks.fr/2017/09/11/ubuntu-gnome-shell-in-artful-day-11/. Some people wants to set it and don’t know how to use update-alternatives properly. I don’t think we have an interface to access and update the host /etc/alternatives, correct? This is why this request for classic confinement here.

I hope that sheds some lights on it!

1 Like

@didrocks Thanks for the details.

What permission can we offer the real snap that would enable it to do the job it needs to do? Can we dig a bit deeper on that?

As I understand it, it needs:

  1. update-alternatives
  2. dconf

In both cases, what specific paths does it need access to for performing those tasks? The best alternative here may well be to simply implement the interfaces you need.

@jdstrand Ideally we’d be able to confine not only to “update-alternatives” but “update-alternatives of that executable”.

@niemeyer
Thanks for the answer!

So, basically, dconf is only the gsettings plug already, so this isn’t what is really blocking on the classic confinement request.

Stands thus update-alternatives. This one access /etc/alternatives/ directory, in particular the gdm3.css alternative file (so it doesn’t match the binary and can’t really, as it’s not gdm, but we want to configure the gdm theme). Alternatives aren’t necessarilly for executables, but abitrary files as well (like the theme one in that case), so a mapping may be difficult to be systematic for that particular case.

Stracing that utility and omitting linkers and such, it seems that it doesn’t access that much files, once you filter the locales out (that you don’t see in non interactive mode for my specific case), remains:
/usr/lib/x86_64-linux-gnu/gconv/gconv-modules.cache and log files /var/log/alternatives.log that we probably want to be written on the system itself, as it impacts the global system state.

@didrocks That sounds relatively straightforward. We probably need read access on /var/lib/dpkg/alternatives/<name>, and write access to /etc/alternatives/<name> and possibly the log file as you suggest. The interface might look similar to:

plugs:
    alternatives:
        names:
            - gdm3.css

The update-alternatives command itself will need to be run with the --altdir, --admindir, and --logdir options pointing to the host filesystem, unless the tool can read its directories from the environment. Can it?

@jdstrand What do you think?

@niemeyer - I took a look at this and I think this needs a good bit of design consideration (sorry for the long reply, but it is necessary).

snap calling update-alternatives directly

First off, I looked at the accesses required if a snap is allowed to run update-alternatives directly and noticed that for --install/–remove/–query/–display/–list without --altdir/–admindir, a snap running update-alternatives itself requires:

/{,usr/}bin/update-alternatives ixr,
# --install /usr/share/gnome-shell/theme/gdm3.css gdm3.css /snap/communitheme/current/share/gnome-shell/theme/communitheme/gnome-shell.css 15
# --remove gdm3.css /snap/communitheme/current/share/gnome-shell/theme/communitheme/gnome-shell.css
/etc/alternatives/gdm3.css{,.dpkg-tmp} rw,
/usr/share/gnome-shell/theme/gdm3.css{,.dpkg-tmp} rw,
/var/lib/dpkg/alternatives/gdm3.css{,.dpkg-tmp} rw,
/var/lib/dpkg/alternatives/ r,
/var/lib/dpkg/alternatives/* r,
/var/log/alternatives.log w,

Using --altdir /var/lib/snapd/alternatives/alt --admindir /var/lib/snapd/alternatives/admin, then the snap would instead need:

/{,usr/}bin/update-alternatives ixr,
# --altdir /var/lib/snapd/alternatives/alt --admindir /var/lib/snapd/alternatives/admin --install /usr/share/gnome-shell/theme/gdm3.css gdm3.css /snap/communitheme/current/share/gnome-shell/theme/communitheme/gnome-shell.css 15
# --altdir /var/lib/snapd/alternatives/alt --admindir /var/lib/snapd/alternatives/admin --remove gdm3.css /snap/communitheme/current/share/gnome-shell/theme/communitheme/gnome-shell.css
/var/lib/snapd/alternatives/admin/ r,
/var/lib/snapd/alternatives/alt/gdm3.css{,.dpkg-tmp} rw,
/usr/share/gnome-shell/theme/gdm3.css{,.dpkg-tmp} rw,
/var/lib/snapd/alternatives/admin/gdm3.css{,.dpkg-tmp} rw,
/var/log/alternatives.log w,

I then thought, “what if we put --altdir, --admindir and --log in in $SNAP_COMMON”? This reduces the apparmor profile changes to:

/{,usr/}bin/update-alternatives ixr,
/usr/share/gnome-shell/theme/gdm3.css{,.dpkg-tmp} rw,

Note, I looked at dpkg in bionic and found that ADMINDIR_ENVVAR can be used for --admindir, but there is nothing for --altdir or --log.

In considering the above, I noticed several considerations with the general approach of letting a snap execute update-alternatives directly:

  1. when using non-default values for --admindir, --altdir and --log, non-snap invocations of ‘update-alternatives’ is unaware of these alternate locations. While the symlink is setup correctly, this could confuse administrators and users. In the case of gdm3.css where there is already an alternative defined in bionic, update-alternatives with default options would be ignorant of the snapd --altdir/–admindir and vice versa
  2. if the snap is running update-alternatives, there is no guarantee that the snap will DTRT on snap removal which could result in a dangling symlink potentially breaking the software (eg, will gdm3 continue to function correctly if /usr/share/gnome-shell/theme/gdm3.css is a dangling symlink?)
  3. if the snap is running update-alternatives, there is no guarantee it will setup the alternative correctly. This could result in a dangling symlink or a malformated/ill-configured file in --admindir
  4. the snap running update-alternatives itself requires 'w’riting out to an arbitrary location for managing the target symlink (eg, /usr/share/gnome-shell/theme/gdm3.css{,.dpkg-tmp}) and this file is not guaranteed to be in the snap’s mount namespace
  5. update-alternatives and its --altdir/–admindir directories don’t exist in the snap’s mount namespace
  6. cannot mediate the update-alternatives ‘priority’ with security policy
  7. update-alternatives is a command only available on dpkg-based systems and doesn’t exist in the core snap or non-dpkg classic systems
  8. the alternatives mechanism could allow for sandbox escape. Eg, in this case, the snap is allowed to put anything into the css which is processed by an unconfined, privileged process. Consider other cases like someone creating an alternative for ‘/etc/bash.bashrc’ which is sourced in the user’s unconfined shell

To address 2-4, we could simply say that the interface needs manual connection and follow the normal process for issuing snap declarations. If we are going this route, 1 is simply addressed by always using the global database. ‘5’ could be handled a number of ways (eg, adjust snap-confine to mount these files/directories from the host or core ships update-alternatives that uses /var/lib/snapd/hostfs/… for its --altdir/–admindir). 6 cannot be addressed since the priority is defined in the file that is placed in the --admindir, which is writable by the snap (ie, have to trust the snap to DTRT). 7 could potentially be addressed by core shipping an update-alternatives that is a no-op on non-dpkg systems.

In all, I think it is possible to allow the snap to do this using:

plugs:
  alternatives:
    gdm3.css:
      original: /usr/share/gnome-shell/theme/gdm3.css
      target: /snap/communitheme/current/share/gnome-shell/theme/communitheme/gnome-shell.css
    other.file:           # 'name' from 'man update-alternatives'
      original: /usr/...  # 'link' from 'man update-alternatives'
      target: $SNAP/...   # 'path' from 'man update-alternatives'

snapd then looks at the above and adjusts the apparmor profile and mount namespace accordingly. The snap then is allowed to do these commands itself to manage the alternative:

# install
(root)$ update-alternatives --altdir /var/lib/snapd/alternatives/alt --admindir /var/lib/snapd/alternatives/admin --install /usr/share/gnome-shell/theme/gdm3.css gdm3.css /snap/communitheme/current/share/gnome-shell/theme/communitheme/gnome-shell.css 15

# remove
(root)$ update-alternatives --altdir /var/lib/snapd/alternatives/alt --admindir /var/lib/snapd/alternatives/admin --remove gdm3.css /snap/communitheme/current/share/gnome-shell/theme/communitheme/gnome-shell.css

Doing it this way is possible, but again, priority is unmanaged by snapd, the system is somewhat brittle, it all requires a decent amount of trust in the snap to DTRT and requires the developer to have a pretty deep understanding of the update-alternatives dpkg mechanism.

snapd calling update-alternatives on the snap’s behalf

A much more robust implementation would have snapd call out to update-alternatives itself which addresses all of the above shortcomings except ‘8’ (confinement escape) since the snap has no additional access to the system alternates files, arbitrary files, etc and because snapd can ensure that the alternate files have sane values and no dangling symlinks. I can think of a few ways to handle this:

  1. snap declares some snap.yaml and calls a snapd-supplied update-alternatives which talks to snapd over DBus/unix socket/something
  2. snap only declares some snap.yaml and snapd takes care of the rest
  3. snapd only understands certain alternatives (eg, only ‘gdm3.css’) instead of being a general purpose alternatives mechanism (potentially address confinement escape depending on how strict we are with adding new alternatives)

IMHO, ‘2’ and ‘3’ are more inline with the design principles of snapd. We could sketch out what this could look like in yaml, but ultimately I think we need to define some requirements and design the whole thing properly. For whoever would pick this up, here are some questions I think need answers before designing the feature:

  • should the system support only specific alternatives or be totally generalized (ie, option ‘3’ vs anything else)
  • should the system support more than just dpkg-based systems?
  • how much of update-alternatives do we want to expose to the user? ‘man update-alternatives’ has a lot of options for example (ie, option ‘1’ vs anything else)
  • can a snap setup an alternative to something that isn’t part of its snap? In the case of communitheme-set-default, it is setting the alternative to point to something in the ‘communitheme’ snap and not ‘communitheme-set-default’
  • should there by a ‘snap alternative …’ command that the user can use to do things, like add/remove/config an alternative that the snap setup?

One thing I failed to mention is that this hostfs path is going to be highly classic-distro dependent. Eg, there is no guarantee that Ubuntu’s gdm3 is the same as Debian’s is the same as Mint’s is the same as…

Note that alternatives are not supported by all distros out there (RH invented it and debian adopted it, so all derivatives of these two will likely have it, but others will/might not). So this feature should be dynamic based on the distro you install on if it is actually inside snapd …

Any update on this or a path forward?
I keep getting requests of people wanting to have that helpers to reset their theme properly and have something to help them.

I guess if that’s not possible as a classic snap, we can envision a debian package though. It’s a little bit for bionic, but I wonder if a ppa + deb for bionic and then package in cosmic is our only alternative?

@niemeyer - I think today we have only two choices: allow classic and this functionality as a snap or disallow classic and @didrocks goes the deb/ppa route. The update-alternatives route would take a good bit of time to design (and the resources to perform that!).

@jdstrand Can we please have a quick call this afternoon to sort this out? Ideally we’d just support update-alternatives, so we don’t need to hand classic to everybody that needs just that.

@didrocks If you are around, you’d be most welcome to join us.

1 Like

@niemeyer sure, just ping me on IRC with a link :slight_smile:

1 Like

@didrocks We quickly talked on IRC and @jdstrand asked to postpone that to Tuesday, so he has time to prepare more details about the potential update-alternatives interface.

@jdstrand, @niemeyer: hey, any update?

Last Friday didn’t work for me; we’ll discuss this soon.

@niemeyer and I had a call today and came up with a scoped design for an update-alternatives interface that should work for communitheme-set-default and as a general mechanism. @niemeyer, please read and approve the design since I had to make a small adjustment (not linking into the mounted squash) and had ideas for future iterations (alternatives for snap commands, link groups).

Design

The overall design is as follows:

  • on systems that have the ‘update-alternatives’ command, an implicit slot named ‘alternatives’ will be provided by snapd (eg implicitOnClassic: osutil.FileExists("/usr/bin/update-alternatives") (probably something smarter ;). Eventually can extend this to other commands that provide similar functionality on other distros
  • snaps desiring to use this mechanism will ‘plugs’ the ‘alternatives’ interface
  • upon interface connect, snapd will call update-alternatives with appropriate arguments and default priority to install the alternative on behalf of the application
  • upon interface disconnect, snapd will call update-alternatives with appropriate arguments to remove the alternative on behalf of the application
  • since the alternatives mechanism allows switching out system data, binaries, etc to that of the snap, the interface will be manually connected (snap declarations can of course override this).
  • to avoid race conditions with a snap’s squashfs mount and use of the alternative, on snap install/refresh snapd will copy the alternative to /var/lib/snapd/alternatives/snap.name (eg, to avoid situations like gdm not starting because it came up before the communitheme-set-default snap was mounted)

Constraints

  • snapd shall not allow creating a brand new alternative and therefore the alternative must already exist (eg, update-alternatives --list gdm3.css must return without error). Put another way, the ‘alternatives’ interface is only allowed to ‘update’. snapd may offer this in the future.
  • snapd shall not support ‘link groups’ initially (where multiple files can be updated as a group) since the target of the symlink cannot be automatically determined. snapd may offer this in the future
  • snapd shall honor ‘automatic’ and ‘manual’ mode and behave like Debian maintainer scripts (ie, update the links as needed if the link group is in ‘automatic’ mode, otherwise it will leave it untouched)
  • the update-alternatives command only works with files, not directories, so the snapd implementation will also only support files, not directories

Store Reviews

As mentioned, the interface will be manually connected. The interface is designed in part to facilitate store reviews where a reviewer should be able to easily see the name of the alternative and what the snap is providing for it. In this manner we can guard against, say, a snap declaring an alternative for ‘vim’ and having it point to a binary in the snap which would be executed outside of confinement.

A future iteration could support the concept of using the alternatives interface with one of the snap’s commands, such that instead of copying the file from the snap to /var/lib/snapd/alternatives/snap.foo.cmd/bar/file, it symlinks /var/lib/snapd/alternatives/snap.foo.cmd/bar/file to /snap/bin/foo.cmd.

Implementation

snap.yaml:

name: foo
plugs:
  alternatives:
    linkname1:
      path: $SNAP/bar
    linkname2:
      path: $SNAP/baz
      priority: 30
    linkname3:
      priority: 30
      command: true            # ??? not implemented in phase 1
    linkname-with-link-group:  # not implemented in phase 1
      group:
      - $SNAP/norf
      - $SNAP/corge
      - ...

apps:
  cmd:
    plugs:
    - alternatives

For a snap with the above snap.yaml, snap install foo will have snapd do:

  1. copy $SNAP/bar from the snap to /var/lib/snaps/alternatives/snap.foo.cmd/linkname1/bar
  2. copy $SNAP/baz from the snap to /var/lib/snaps/alternatives/snap.foo.cmd/linkname2/baz

For a snap with the above snap.yaml, snap connect foo:alternatives will have snapd do (pseudo code):

for lname in linkname1 linkname2:
    # make sure the alternative exists
    if ! `update-alternatives --list <lname>`:
        continue

    priority = 20
    if lname specified priority:
        priority = <lname's priority>

    # link source from the snap
    path = /var/lib/snapd/alternatives/snap.foo/<lname>/<lname's path>

    # auto-detect the link target from existing alternative on the system
    target = `update-alternatives --display <lname>` | grep '  link <lname> is ' | awk '{print $4}'`

    # add the snap alternative for the detected link target
    `update-alternatives --install <target> <lname> <path> <priority>`

(a proper implementation would detect the ‘target’ much more robustly :wink: )

For a snap with the above snap.yaml, snap disconnect foo:alternatives will similarly have:

for lname in linkname1 linkname2:
    path = <lname's path>
    `update-alternatives --remove <lname> <path>`

With the above implementation, the communitheme-set-default snap would specify:

plugs:
  alternatives:
    gdm3.css:
      path: $SNAP/path/to/my.css
      priority: 15

apps:
  communitheme-set-default:
    ...

Then on snap install communitheme-set-default, snapd:

  1. copies $SNAP/path/to/my.css from the snap to /var/lib/snapd/alternatives/snap.communitheme-set-default.communitheme-set-default/gdm3.css/my.css

Then on snap connect communitheme-set-default:alternatives, snapd:

  1. checks if ‘gdm3.css’ is an alternative, if so…
  2. update-alternatives --install /usr/share/gnome-shell/theme/gdm3.css gdm3.css /var/lib/snapd/alternatives/snap.communitheme-set-default.communitheme-set-default/gdm3.css/my.css 15

Then on snap disconnect communitheme-set-default:alternatives, snapd:

  1. update-alternatives --remove gdm3.css /var/lib/snapd/alternatives/snap.communitheme-set-default.communitheme-set-default/gdm3.css/my.css

Implementation notes:

The ‘alternatives’ interface (snapd.git/interfaces/built/alternatives.go) should use a new ‘alternatives’ backend (snapd.git/interfaces/alternatives/…) where alternatives.go calls out to it.

1 Like

Any progress on the implementation and a potential ETA for releasing? (Note that the request was initially opened 2 months ago)

I don’t know if this is assigned to anyone (I suspect not yet, but @niemeyer can comment authoritatively). That said, now that there is a design, the implementation can be done by anyone.

I have one concern / question:

Should Yaru ever take over as the default theme you “must” consider implementing plymouth in to the theme as well. The problem with that would be:

update-grub2
update-initramfs

I am working on a way to change up some colors and was waiting for this implementation, but as I was going to include plymouth this would still require a classic confinement.