Layouts: re-mapping snap directories

As a snap author you will have some new syntax you can put at the top-level of your snapcraft.yaml or snap.yaml. Let me start with a simple example that we shall discuss below:

snap.yaml and snapcraft.yaml syntax

name: my-fun-snap
version: 1.0
apps:
  my-fun-snap:
    command: ...
...
layout:
  /usr:
    bind: $SNAP/usr
  /mytmp:
    type: tmpfs
    user: nobody
    group: nobody
    mode: 1777
  /mylink:
    symlink: /link/target

As you can see layout is a top-level construct that describes filesystem entries. Each entry can be of one of the three inferred types: a bind mount, a tmpfs mount or a symlink.

Bind mount

Bind mounts are perhaps the most familiar and the first to arrive soon. The declaration above will hide anything that is usually present in /usr and replace it with what the snap ships in its $SNAP/usr/ directory. The variable $SNAP is expanded to a path such as /snap/my-fun-snap/42/ internally.

If you never played with bind mounts you can experiment on your own system using the mount --bind /source /target command (you can unmount /target later). Keep in mind that bind mounts are not copies or symlinks. Anything that is visible from /source becomes visible from /target. Notably both /source and /target must exist (even if they are just empty directories) as mount needs to attach to an existing file or directory.

If there are other things mounted in /source, for example if /source/data is a mounted USB stick the way the mount --bind command is issued affects what will show up in /target/data. If the bind mount is recursive then all of the things mount that were mounted in /source at the time the command was issued will also show up in /target. On command line this can be done by using mount --rbind which stands for recursive bind.

After the bind is done subsequent mount operations can be done both inside /source and /target and the behavior of those changes depending on the type of sharing that is set up. In simple terms when we mount or unmount /source/data this event can propagate to peer groups (such as /target). The event can propagate both ways (shared) from master to slaves only (master and slave) or not at all (private). This allows complex arrangements where events only propagate in certain directions, or even to things that are not visible from the point of view of the process inhabiting a given mount namespace. You don’t necessarily have to use all of those features but it is worth remembering they exist.

For the curious readers this is described in detail in the Linux kernel documentation https://www.kernel.org/doc/Documentation/filesystems/sharedsubtree.txt (the keyword is “shared subtree”).

The bind layout element will allow only a subset of this complexity, namely recursive, shared mounts. This is the most intuitive behavior that fits most use cases.

Interestingly, the source directory may refer to places other than $SNAP. In fact, most arbitrary paths can be used. There will be some limitations (described below), like /media but in general unless you are trying to be malicious the system should not get in your way.

The target directory can already exist but will be created automatically if necessary. The user, group and mode attributes can be used to define fine-grained details about the created parent directories. The technical approach to creating new entires on top of a read only substrate, such as a squashfs filesystem, will be described in the implementation notes in a subsequent post. From the developer’s and users point of view it will just work.

TempFS

The tmpfs element are also very familiar. They will allow snap developers to put a new, ephemeral, temporary file system at the desired location. The same semantics regarding variable expansion (e.g. $SNAP, $SNAP_COMMON or $SNAP_DATA), creation of parent directories, application of user, group and mode (permissions).

Use of tmpfs can be beneficial to explicitly create empty spaces, e.g. for small amounts of ephemeral data, lock files, pid files or other typical things. Keep in mind that tmpfs is backed by RAM and can be very constrained. At this time there is no syntax to describe the size limits on mounted tmpfs but such keyword may be added later.

Symlink

The symlink element can be used to create a symlink rooted at the designated location and pointing at the specified target. The target doen’t have to exist (the symlink can be broken). The same semantics regarding creation of parent directories, user and group ownership and permissions mentioned earlier applies.

Syminks or bind mounts? What is appropriate for me?

Symlinks and bind-mounts have similar “feeling” but they behave differently. Symlinks are very common and many applications can detect them. In comparison bind mounts are not as easy to detect so they have higher chance of being transparent to the application. The downside is that a mount point (/target) cannot be removed before it is unmounted while many applications can simply unlink a symbolic link easily.

Bind mount gotchas

Lastly bind mounts have some, say, unusual semantics when the source is deleted. Let’s look at a simple example

root@fyke:~# mkdir experiment
root@fyke:~# cd experiment/
root@fyke:~/experiment# mkdir source
root@fyke:~/experiment# mkdir target
root@fyke:~/experiment# mount --bind source target
root@fyke:~/experiment# ls -l
total 8
drwxr-xr-x 2 root root 4096 lip 28 15:44 source
drwxr-xr-x 2 root root 4096 lip 28 15:44 target
root@fyke:~/experiment# cat /proc/self/mountinfo | egrep 'source|target' 
139 25 8:2 /home/zyga/experiment/source /home/zyga/experiment/target rw,relatime shared:1 - ext4 /dev/sda2 rw,errors=remount-ro,data=ordered
root@fyke:~/experiment# rmdir target
rmdir: failed to remove 'target': Device or resource busy
root@fyke:~/experiment# rmdir source
root@fyke:~/experiment# cat /proc/self/mountinfo | egrep 'source|target' 
139 25 8:2 /home/zyga/experiment/source//deleted /home/zyga/experiment/target rw,relatime shared:1 - ext4 /dev/sda2 rw,errors=remount-ro,data=ordered
root@fyke:~/experiment# touch target/file
touch: cannot touch 'target/file': No such file or directory
root@fyke:~/experiment# umount target
root@fyke:~/experiment# ls -l
total 4
drwxr-xr-x 2 root root 4096 lip 28 15:44 target
root@fyke:~/experiment# rmdir target

When the target is mounted it cannot be removed. This restriction does not apply to source but once removed the target is, well, broken. It’s still a mount point but it no longer exists so we cannot create anything inside it anymore (anything that used to be there would have to be removed before the source would be possible to remove anyway so it is always empty).

5 Likes