Classic?
Before we can discuss today’s topic I need to untangle a rather confusing concept. Snapd cares about naming a lot. We really try to do our best to name things in a consistent and clear way. We don’t like to create error messages from hell and force the user to google crap to understand what the computer just said.
Well, we didn’t get one thing right though: the term classic, currently, may refer to:
- classic distribution that uses an existing package system (debs/rpms) in addition to snaps
- classic snap that one can install on Ubuntu core (which is not a classic distribution as it uses snaps exclusively) to get classic Ubuntu with apt-get and writable-anywhere file system
- classic confinement that a snap can request to effectively have no confinement at all (like in a classic distribution)
Today we’re talking about that last thing.
About classic confinement
Classic confinement is a very simple idea. Move your application into a snap easier than before by taking one element out of the equation. Disable confinement and file system redirection entirely to get started faster (and by get started, I mean get started with making a snap).
This has the advantage of still having access to snap features such as channels
and a way to update the software easily and has some sense of security through
the assertion system that at ensures that we know where the software is coming
from. This alone makes it better than curl | sh -c that many upstream
projects recommend as their official installation instructions. Even if someone
goes rogue / malicious and betrays our initial trust, we can smite such
offenders (or the offending package revisions) and pull the snap from the store
as fast as possible in order to limit damage.
Digression: We (the snapd team) are paranoid about security and even when the system is no longer shielded from the application the store has a strong policy for well-behaved, well-known FOSS projects as the only allowed providers of snaps using classic confinement. In many ways this is similar to what you get with regular Debian packages (most of them are not using confinement and even if they do, they must do so voluntarily) except for the all-stop freeze-the-archive periods and a way to reuse dependencies from the distribution.
Your application can rely on the core snap and on whatever it brings by itself and is free to try to use anything on the classic distribution file system but if it does then all bets are off.
So far so good. I personally think that it is a great way to deliver developer tools like compilers or interpreters or system administration tools. Anytime where you want them to operate on your system as it really exists and not be stuck in some hidden complex machinery that ensures uniformity across diverse systems.
The idea is pretty good but as things stand today it is not that easy to use.
Technical problem
Technically when snap-confine helps to run an application using classic
confinement it behaves in a special way. It no longer enables apparmor, apply a
seccomp profile, do any tricks with mount name spaces or bind mounts. It is
just a pass-through to your application.
Building classic-confinement snaps with snapcraft was (and I think, still is) a
bit bumpy. When we initially discussed this problem we realized that without
control of the mount name space we must rely on building the software in a way
that would force it to use resources from the core snap before it tried to look
at the likes of /lib and /usr which may contain entirely incompatible stuff
and lead to crashes.
We set a few simple assumptions here:
- the executable must use the dynamic linker from the core snap
- the executable must be linked to the libraries from the core snap or from its own snap
- the executable must be built in a way that lets it find resource files in its own snap.
With those three assumptions you and the bag of predictable low-level libraries in the core snap you can run your software on pretty much any Linux distribution.
All you need to do is to provide a few build-time configuration options, a few switches to the linker and you’re done. This means that you cannot easily reuse pre-build software. Why? Because it would have none of the magic that makes it do the three things I just listed.
People tried and ran into this issue pretty quickly. If you’re familiar with how things work at that level and you feel comfortable with building your software from source (and you should be if you want to build a snap, after all, it is your software, snaps empower deveopers to ship software) you could build the software from source, with just the right set of options in order to avoid it but it doesn’t feel like a very good and easy story.
If classic confinement was not useful as an entry into the world of snaps it makes little sense to spend time and effort on. We started looking for options and after a few discussions ran into this idea.
Plan B
Since launchers that allow you to run any snap application are managed by snapd, we can use them to redirect the dynamic linker on unmodified binaries.
Technically we can make the snap run -> snap-confine -> snap-exec chain
do what we want. At the very last stage snap-exec, instead of just running
the application, we can run the dynamic linker with command line option to
change the search path to just the core snap and the application’s own snap.
Then, for as long as the application doesn’t need to run extra executables from
the core snap or from its own snap, things should generally work.
There will still be rough edges. Pre-compiled software will still load data
from /usr/share, look at configuration files in /etc (possibly this is not
a problem but it depends on fine details). The most serious problem is with
applications that internally run other programs. If they attempt to run a
program from their own snap (which is perfectly reasonable thing for a snap to
do) two things will happen:
- the kernel will resolve the dynamic linker to use
- the linker will resolve the dynamic libraries to link
Both of those will not be the carefully overridden values we need to provide to
make things work. What is even worse is that because we don’t want to pollute
the environment with variables such as LD_LIBRARY_PATH or PATH (as that
would mostly prevent applications from the classic distribution from working)
the child will no longer be able to run anything from the snappy world without
having to pass through the generated application launchers.
Plan B+
One way of tackling that problem would be to enumerate all the executables in the core snap and in the snap that is being started and generate internal launchers for all the executables seen there. Then a directory with all such launchers could be placed on PATH so that they would be found when searched for. The launchers would just ensure the linker switch happens when it is required.
Still this is not perfect. What if an application happens to try to run an
executable with the full path? Say /snap/core/current/bin/true. Then we are
back to square one and all the problems resurface.
Plan B++
One, perhaps somewhat invasive, option would be to bind mount the shadow launchers over all the executables in the core snap and in all the snaps that are using classic confinement. Then there would be no way to run anything without going through a part of snapd that makes the magic happen.
I think this would be a bit messy though. The output of mount might make a
sysadmin raise an eye-brow or reconsider to move to the countryside and herd
sheep.
What is the price that needs to be paid to maintain the illusion of seamless access to any pre-compiled application binary running on any Linux distribution?
Summary
It is possible to create snaps that use classic confinement and they will work correctly across a wide variety of environments but the process is not as easy as we would like.
If you build your software from source use:
--prefix=/snap/$SNAP_NAME/current/usr and re-locate the code so that on
install only the usr directory is present in the snap. Using snapcraft, or
directly, use the right dynamic linker (-Wl,--dynamic-linker=...). Use the
libraries from the core snap using rpath (-Wl,rpath,...) and things will just
work.
As soon as we make the changes you can drop the last two elements and perhaps even switch to pre-build software from another source. I will post an update when this happens. All the existing snaps using classic confinement will work as before but new builds can then drop the extra complexity.