The new classic confinement in snaps – Even the classics need a change

by Igor Ljubuncic on 24 June 2022

As part of their fundamental, security-driven design, snaps are meant to run isolated from the underlying system. In most cases, the idea works well, and granular access to system resources using the mechanism of interfaces allows snap developers to ship their applications packaged with strict confinement.

However, there are some scenarios where even the liberal use of interface plugs cannot fully satisfy all of the functional requirements of specific applications. Certain programs need system-wide access to directories and files, and others may need to execute arbitrary binaries as part of their run. To that end, snaps can also be installed in the “classic” confinement mode, which gives them access similar to what the application would have if installed in the traditional way. The solution works, but now, there are proposals to make the classic mode even more robust and efficient.

The clash of shared libraries

The problem with classic confinement is that it takes away some of the predictability that exists in strictly confined snaps. Whereas one should expect a snap to behave the same way on all supported systems, classic snaps may rely on the host’s libraries to run, or may assume that certain libraries (and/or specific versions) are present. This can lead to potential conflicts in how applications are loaded and executed.

In order to run correctly, classically confined snap packages should require dynamic executables to load shared libraries from the appropriate base snap instead of using the host’s root filesystem. This means classic snaps would behave more like strictly confined snaps, which should lead to higher consistency and predictability of execution.

A new proposal in the works describes what is needed to verify dynamic linking parameters in binary files in a classic snap package.

Library path detection, better binary patching

Classic snap packages run on the host’s root filesystem, which may not match their build environment. To prevent incompatibilities, binaries in classic snaps must be built with appropriate linker parameters, or patched to allow loading shared libraries from their base snap. In the case of potential dynamic linking issues, the snap author must be aware that their package may not run as expected.

With the new proposal, the following dynamic linking parameters need to be covered:

  • Runtime library paths – The dynamic section of an ELF file contains the RPATH entry, which lists the runtime paths to shared libraries that are to be searched before the paths set in the LD_LIBRARY_PATH environment variable and the RUNPATH entry. Multiple paths separated by a colon can be specified.
  • $ORIGIN path – The special value $ORIGIN represents the path where the binary is located, thus allowing the runtime library path to be set relative to that location (e.g.: $ORIGIN/../lib for an executable installed under bin/ with libraries in lib/). 
  • File interpreter – The special ELF section .interp holds the path to the program interpreter. If used, it must be set to the path of the appropriate dynamic linker: the dynamic linker from the snap package being created If libc is staged, or the dynamic linker provided by the base snap otherwise. Developers should be aware that binaries linked against an alternative libc may use interpreter values pointing to a different dynamic linker.

Affected files and new rpath

To execute as expected, binaries in a classic snap application must be configured to look for shared libraries provided by the base snap or bundled as part of the application snap. This is achieved by setting the runtime path to shared libraries in all ELF binaries (except relocatable object files) that are present in the package payload.

The ELF file rpath must be properly set if:

  1. The project has a base; and
  2. The project’s base is not bare; and
  3. Confinement is classic, or libc is staged

The rpath value must be set to reach all NEEDED entries in the dynamic section of the ELF binary.  If the binary already contains an rpath, then it should keep only those that mention $ORIGIN. Rpath entries that point to locations inside the payload must be changed to be relative to $ORIGIN. However, this does not include or support detecting paths to shared libraries loaded with dlopen().

There are various strategies to set rpath and the interpreter.

Setting rpath at build time

An ELF binary created during the parts lifecycle execution can have its rpath value set by using appropriate linker parameters. The linker is typically invoked indirectly via a compiler driver; in the gcc case, parameters can be passed to the linker using the -Wl option:

$ gcc -o foo foo.o -Wl,-rpath=\$ORIGIN/lib,--disable-new-dtags -Llib -lbar

A similar strategy can be used to set rpath in a cgo binary:

package main
/*
#cgo LDFLAGS: -L${SRCDIR}/lib -Wl,-rpath=\$ORIGIN/lib -Wl,--disable-new-dtags -lbar
#include "bar.h"
*/

import "C"

func main() {
    C.bar()
}

In both cases, the inspection of the ELF dynamic section contents reveals the rpath value has been properly set:

Tag                Type             Name/Value
0x0000000000000001 (NEEDED)         Shared library: [libbar.so]
0x0000000000000001 (NEEDED)         Shared library: [libc.so.6]
0x000000000000000f (RPATH)          Library rpath: [$ORIGIN/lib]

Patching generated executables

A snap payload may contain pre-built ELF binaries installed from arbitrary sources (typically from the distribution archive, after installing stage packages). In this case, rpath must be set by modifying the existing binary using a tool such as patchelf:

$ patchelf --force-rpath --set-rpath \$ORIGIN/lib foo
$ readelf -d a | grep RPATH
0x000000000000000f (RPATH)              Library rpath: [$ORIGIN/lib]

Patchelf can also be used to change the interpreter to a different dynamic linker:

$ patchelf --set-interpreter /lib64/ld-linux-x86-64.so.2 foo
$ readelf -p .interp a
String dump of section '.interp':
[     0]  /lib64/ld-linux-x86-64.so.2

Issues and limitations

Of course, there is no guarantee that every single binary can be patched, or that all use cases will be covered. Patching ELF binaries to modify rpath or interpreter entries may fail in certain scenarios, such as go executables linked with the go linker, or binaries using libc variants that require a nonstandard interpreter. Additionally, patching will cause signed binaries to fail validation.

Classic snap linting

As part of the improved developer experience, the snapcraft tool will also provide linter warnings, which should help the snap creators understand more accurately if there are any potential issues with the building of the snap using classic confinement. Specifically, the linter should issue warnings if the payload contains binaries that can load potentially incompatible shared libraries.

  • Patching is required, and the ELF binary interpreter is not set to the correct dynamic linker (from base or staged libc). Special handling may be needed if the ELF binary uses a nonstandard interpreter (in which case the linter would otherwise issue a false positive warning).
  • Patching is required, and the ELF binary rpath is not set to the value needed to load shared library dependencies from the base, or from the snap if the dependency is not part of the base.
  • In addition to these warnings, the snapcraft tool could also print out information on how to set parameters or patch existing binaries at build time.
  • Snapcraft may also perform ELF binary patching automatically according to parameters set in the project configuration file. Regardless of the default policy, the option to not perform automatic patching must be present due to limitations on the patching mechanism, or because the existing values should not be changed.

Summary

The classic confinement scenario is a difficult one, as it needs to provide repeatable, consistent behavior and results in an unpredictable environment (the user’s machine). The Snapcraft team is trying to make the experience as elegant as possible, so that developers can reliably package their applications as classic snaps, and ship them to their users. In this article, we covered some of the tools and methods used to improve this functionality. If you have any questions or ideas regarding classic snaps, please join our forum and tell us what you think.

Photo by Rustyness on Unsplash.

Newsletter Signup

Related posts

Snapcraft 8.0 and the respectable end of core18

‘E’s not pinin’! ‘E’s passed on! This base is no more! He has ceased to be! ‘E’s expired and gone to meet ‘is maker! ‘E’s a stiff! Bereft of life, ‘e rests in peace! If you hadn’t nailed ‘im to the perch ‘e’d be pushing up the daisies! ‘Is software processes are now ‘istory! ‘E’s […]

Craft team welcomes you to another episode of its adventures

Welcome to the second article in the Craft team saga. Previously, on Craft Team, we gave you a brief introduction into the team’s function, we announced our desire to share the ins and outs of our day-to-day work with the community, and gave you an overview of roughly two weeks of coding and fun. Today, […]

The long ARM of KDE

With over 100 applications available in the Snap Store, KDE is by far the biggest publisher of snaps around. What unifies this impressive portfolio is the fact that all of these snaps are made for the x86 platform. Not anymore. Now, don’t panic! The x86 snaps are not going anywhere. But ARM-supported KDE snaps are […]