Skip to content

App sandbox

An app says what it needs from a sandbox in the [sandbox] table of Sabine.toml, using the names Flatpak uses. A store or the system can show the declaration before the app is installed, and enforce it when the app starts. On Linux, Sabine enforces it itself when nothing else does.

[sandbox]
network = true
filesystems = ["xdg-documents", "xdg-download:ro"]
devices = ["usb"]
talk = ["org.mpris.MediaPlayer2.notes"]
Key Type Default Meaning
network boolean true Whether the app goes online
filesystems array of strings none Folders the app reaches: home, xdg-desktop, xdg-documents, xdg-download, xdg-music, xdg-pictures or xdg-videos. Add :ro for read-only access, such as xdg-download:ro
devices array of strings none usb, serial or all
talk array of strings none Session bus names the app calls, such as org.mpris.MediaPlayer2.notes. A trailing .* covers every name below it
unsandboxed boolean false Declares that the app can’t run in a sandbox
reason string One line, shown to whoever decides, explaining why the app can’t run in a sandbox. Only with unsandboxed = true

Files a person picks in a file dialog reach the app without any filesystems entry, and so do the app’s own folders. A place or device may appear only once, and xdg-music and xdg-music:ro together count as twice.

talk refuses names that would let an app leave its sandbox: the portal backends and permission store (org.freedesktop.impl.portal.*), org.freedesktop.portal.Flatpak, org.freedesktop.Flatpak, org.freedesktop.systemd1 and org.freedesktop.DBus, and wildcards that cover any of them, such as org.freedesktop.*. Names must be valid D-Bus names with at least two elements.

An app that needs more than a sandbox allows, such as one that manages containers or the system itself, says so instead, with a reason:

[sandbox]
unsandboxed = true
reason = "It manages the containers Podman runs for you."

An unsandboxed declaration takes only unsandboxed and reason; any other key is an error, and so is a reason without unsandboxed = true.

sabine dev and sabine bundle check the table and stop on a mistake, and unknown keys are an error. Without a [sandbox] table the app declares nothing.

Every bundle carries the declaration, normalized, in the production manifest that a store or the system reads without starting the app:

Bundle Manifest
AppImage, deb, rpm, Linux tarballs usr/lib/sabine/<app id>/resources/Sabine.toml
macOS Contents/Resources/Sabine.toml
Windows resources/Sabine.toml next to the program

On Linux, a system that runs the app in a sandbox of its own enforces the declaration; Luft’s Schelf does this for AppImages. Everywhere else, Sabine enforces it as the app launches, with Landlock and a system call filter that every process the app starts inherits, Chromium included. It needs neither root nor bubblewrap.

A confined app:

  • reads the system’s programs, libraries and settings, and the person’s fonts, icons, themes and desktop settings
  • writes the folders named after its app id in the XDG data, config, cache and state folders, its Chromium profile, and the temporary and runtime folders
  • reads its own program, web assets, Sabine’s host and the CEF runtime, and writes Sabine’s logs
  • reaches the declared filesystems and devices, and nothing else in your home folder
  • can’t reach abstract sockets or signal processes outside the app
  • with network = false, can’t open or accept TCP connections, apart from its dev server and DevTools port in development

The system call filter also refuses the kernel keyrings and log, ptrace and other processes’ memory, and typing into or taking over the terminal the app was started from.

A browser or other program the app starts is confined with it, so a confined app opens links and files through the desktop portal (shell.openLink and shell.showFolder do this already). Each kernel applies what its Landlock version supports; a kernel without Landlock gets only the system call filter, and the app records that once in its diagnostics.

The session bus isn’t filtered yet, so talk is declared but not enforced by Sabine. Apps with unsandboxed = true, apps without a [sandbox] table, and apps started with SABINE_FULL_ACCESS set run unconfined. A system sets SABINE_FULL_ACCESS for an app the person gave full access.

Inside a sandbox that marks its processes with /.flatpak-info, as Flatpak does, the app runs its own Chromium in the sandbox and never uses the shared runtime. Chromium can’t create the namespaces of its renderer sandbox there, so when the sandbox provides Zypak in ZYPAK_BIN, Sabine starts Chromium through zypak-helper, which starts Chromium’s helpers through the Flatpak spawn portal, each in a smaller sandbox of its own. Renderers keep their namespaces and system call filter. The runtime and host have to be visible inside the helpers’ sandbox, so a sandboxed app uses a runtime installed for every user or bundled with it.

Sabine enforces [sandbox] as described above.

[sandbox] confines your app’s own process and everything it starts. Chromium’s sandbox is a separate layer that confines web content inside Chromium, and Sabine never turns it off. Pages of apps in the shared runtime run in that runtime’s Chromium, which only Chromium’s own sandbox confines.

Chromium uses its namespace and seccomp sandbox. Sabine doesn’t turn it off when the system’s policy keeps it from creating namespaces; see AppArmor.

Ubuntu and other distributions that restrict unprivileged user namespaces with AppArmor stop Chromium from creating its sandbox for hosts installed in your home folder. sabine runtime doctor reports the host failing its launch check. Generate a profile that allows it as the user who runs Sabine, then have an administrator install it:

  1. Generate the profile. This prepares the runtime and host first if they’re missing.

    Terminal window
    sabine runtime sandbox-profile > sabine-apparmor
  2. Review the paths in it, then install and load it:

    Terminal window
    sudo install -m 644 sabine-apparmor /etc/apparmor.d/sabine-$USER
    sudo apparmor_parser -r /etc/apparmor.d/sabine-$USER
  3. Check that Chromium starts, and clean up:

    Terminal window
    sabine runtime doctor
    rm sabine-apparmor

The profile allows user namespaces for every sabine-host under your Sabine data folder, so it keeps working as the runtime updates, plus a separate entry for the host in use when it lives elsewhere. It changes no global AppArmor policy. Anyone who can replace an executable at those paths can also use this permission, which is why an administrator should review them.

If the kernel disables user namespaces entirely, an administrator has to enable them or provide Chromium’s root-owned setuid helper through CHROME_DEVEL_SANDBOX. Running apps as root isn’t supported.