Bubblewrap

From Alpine Linux
Revision as of 00:15, 6 September 2023 by Encode (talk | contribs) (Remove note about needing to pass the source for 'config' if it's a symbolic link. That does not appear to be needed.)
This material is work-in-progress ...

The reasoning is most likely wrong for why to do some stuff. Someone more experienced needs to look it over.
(Last edited by Encode on 6 Sep 2023.)

Bubblewrap is an unprivileged sandboxing tool. Kernel features it also has: User/IPC/PID/Network/UTS/cgroup namespaces and Seccomp filters.

How bubblewrap works, as stated in the README.md:

bubblewrap works by creating a new, completely empty, mount namespace where the root is on a tmpfs that is invisible from the host, and will be automatically cleaned up when the last process exits. You can then use commandline options to construct the root filesystem and process environment and command to run in the namespace.

Installation

Install bubblewrap:

# apk add bubblewrap

Note: The package is bubblewrap but the command to manage it is bwrap.

How to workout what a program needs

Look at Bubblewrap/Examples to see various ways bubblewrap can be used.

Prerequisites

First make sure to have a user editable directory in "$PATH". This page will use "${HOME}/.local/bin/", create it if it does not exist:

$ mkdir -p ~/.local/bin

Add it to ~/.profile:

Contents of ~/.profile

... PATH="${PATH}":"${HOME}/.local/bin" export PATH ...

Will need to relog for this to apply.

Basic bwrap setup

Note: With how we will be sandboxing everything that doesn't match our owner/group will show as nobody.

Lets assume you want to sandbox imv and are using Wayland only. Here is how you might go about that.

Create bwrap-imv inside "${HOME}/.local/bin/" and make it executable:

$ touch ~/.local/bin/bwrap-imv $ chmod 0700 ~/.local/bin/bwrap-imv

Use file to determine the file type of /usr/bin/imv:

$ file /usr/bin/imv /usr/bin/imv: POSIX shell script, ASCII text executable

Use less to view it:

$ less /usr/bin/imv

Contents of /usr/bin/imv

#!/bin/sh if [ -n "${WAYLAND_DISPLAY}" ]; then exec /usr/libexec/imv-wayland "$@" else exec /usr/libexec/imv-x11 "$@" fi

Since we are assuming Wayland only we can just skip to /usr/libexec/imv-wayland. Run file on it:

$ file /usr/libexec/imv-wayland /usr/libexec/imv-wayland: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib/ld-musl-x86_64.so.1, stripped

It is an Executable and Linkable Format (ELF) file. So we know we need the ELF interpreter /lib/ld-musl-x86_64.so.1. We also know we need /usr/libexec/imv-wayland, since it has to know where the command is located. As the argument to /usr/libexec/imv-wayland, put "${@:-./}", this will generate a separate word for each positional parameter and if you didn't have any parameters it will default to current directory.

Running ldd to find all necessary libs, except ones loaded at runtime, on /usr/libexec/imv-wayland, outputs a lot of things but for the moment all we care about are the directory paths. Specifically the starting directories, which are /lib/* and /usr/lib/*.

Warning: The ldd manpage talks about some security implications. It may not apply since they seem to be talking about glibc and musl-utils makes /lib/ld-musl-x86_64.so.1 ldd [1]. Is this something to worry about?


Since this is a shell script, lets use a helpful command:

set -u

if the shell tries to expand an unset parameter, it will error (with a few exceptions).

This is for a GUI Wayland program, so lets also add some prerequisites:

--setenv XDG_RUNTIME_DIR "$XDG_RUNTIME_DIR" \

for determining the directory for the wayland socket;

--setenv WAYLAND_DISPLAY "$WAYLAND_DISPLAY" \

for determining the socket;

 --ro-bind "${XDG_RUNTIME_DIR}/${WAYLAND_DISPLAY}" "${XDG_RUNTIME_DIR}/${WAYLAND_DISPLAY}" \

mount readonly.

Lets also add some nice to haves:

--unshare-all \

will create a new user/ipc/pid/net/utc namespaces and try to create a new cgroup namespace if possible;

--new-session

will create a new terminal session for the sandbox, disconnecting from the controlling terminal so for example it can't inject input into the terminal;

 --die-with-parent

will ensure child process (imv-wayland in this case) dies when bwrap parent dies.

Since we are not passing the whole filesystem, we need a way to pass the arguments given:

--ro-bind "${@:-./}" "$(realpath "${@:-./}")" \

this will take all arguments or the current directory and mount to the absolute path.

Warning: This will have everything under the current directory available to imv if you don't specify an argument. I don't know how to limit it so you can specify 2+ and not have everything.


We might also have a config file:

--ro-bind-try "${XDG_CONFIG_HOME}/imv/config" "${XDG_CONFIG_HOME}/imv/config" \

this will add your local config to imv if you have one and if not will still continue.

Pass "$XDG_CONFIG_HOME" to the sandbox:

--setenv XDG_CONFIG_HOME "$XDG_CONFIG_HOME" \

but this isn't always defined, so lets fallback to the XDG Base Directory default:

XDG_CONFIG_HOME="${XDG_CONFIG_HOME:=$HOME/.config}"

this will use "$XDG_CONFIG_HOME" if it's set, otherwise fallback to the default of "$HOME/.config".

Also need:

--dev-bind /dev/null /dev/null \

and

--ro-bind /bin/sh /bin/sh \

to make use of your config.

Todo: Document why this is needed. This was found by: Bubblewrap#Can't_find_what_path_is_missing, any better way?


~/.local/bin/bwrap-imv now looks like:

Contents of ~/.local/bin/bwrap-imv

#!/usr/bin/env sh # imv wrapped in bwrap. set -u XDG_CONFIG_HOME="${XDG_CONFIG_HOME:=$HOME/.config}" /usr/bin/bwrap \ --unshare-all \ --new-session \ --die-with-parent \ --setenv WAYLAND_DISPLAY "$WAYLAND_DISPLAY" \ --setenv XDG_CONFIG_HOME "$XDG_CONFIG_HOME" \ --setenv XDG_RUNTIME_DIR "$XDG_RUNTIME_DIR" \ --ro-bind /bin/sh /bin/sh \ --dev-bind /dev/null /dev/null \ --ro-bind-try "${XDG_CONFIG_HOME}/imv/config" "${XDG_CONFIG_HOME}/imv/config" \ --ro-bind /lib/ /lib/ \ --ro-bind "${XDG_RUNTIME_DIR}/${WAYLAND_DISPLAY}" "${XDG_RUNTIME_DIR}/${WAYLAND_DISPLAY}" \ --ro-bind /usr/lib/ /usr/lib/ \ --ro-bind /usr/libexec/imv-wayland /usr/libexec/imv-wayland \ --ro-bind "${@:-./}" "$(realpath "${@:-./}")" \ /usr/libexec/imv-wayland "${@:-./}"

Now lets run bwrap-imv; go into a directory with an image:

$ bwrap-imv IMAGE xkbcommon: ERROR: failed to add default include path /usr/share/X11/xkb Assertion failed: keyboard->context (../src/keyboard.c: imv_keyboard_create: 20)

Add:

 --ro-bind /usr/share/X11/xkb/ /usr/share/X11/xkb/ \

to bwrap-imv. XKB is a keyboard keymap support library.

After adding the above, run it again:

$ bwrap-imv IMAGE libEGL warning: wayland-egl: could not open /dev/dri/renderD128 (No such file or directory)

Add:

--dev-bind /dev/dri/renderD128 /dev/dri/renderD128 \

and run again:

$ bwrap-imv IMAGE libEGL warning: wayland-egl: drmGetMagic failed

If you follow Bubblewrap#Can't_find_what_path_is_missing, you will eventually get down to:

--ro-bind /sys/dev/char/ /sys/dev/char/ \}}

and

--ro-bind /sys/devices/pci0000:00/ /sys/devices/pci0000:00/ \
Todo: Document why this is needed. This was found by: Bubblewrap#Can't_find_what_path_is_missing, any better way?


--clearenv \

can also be added now. This will unset all environment variables, except for "$PWD" and any we set with --setenv.

Now imv should show images and your config file should work. If you do not use commands, the finished ~/.local/bin/bwrap-imv should look like:

Contents of ~/.local/bin/bwrap-imv

#!/usr/bin/env sh # imv wrapped in bwrap. set -u XDG_CONFIG_HOME="${XDG_CONFIG_HOME:=$HOME/.config}" /usr/bin/bwrap \ --unshare-all \ --new-session \ --die-with-parent \ --clearenv \ --setenv WAYLAND_DISPLAY "$WAYLAND_DISPLAY" \ --setenv XDG_CONFIG_HOME "$XDG_CONFIG_HOME" \ --setenv XDG_RUNTIME_DIR "$XDG_RUNTIME_DIR" \ --ro-bind /bin/sh /bin/sh \ --dev-bind /dev/dri/renderD128 /dev/dri/renderD128 \ --dev-bind /dev/null /dev/null \ --ro-bind-try "${XDG_CONFIG_HOME}/imv/config" "${XDG_CONFIG_HOME}/imv/config" \ --ro-bind /lib/ /lib/ \ --ro-bind /sys/dev/char/ /sys/dev/char/ \ --ro-bind /sys/devices/pci0000:00/ /sys/devices/pci0000:00/ \ --ro-bind "${XDG_RUNTIME_DIR}/${WAYLAND_DISPLAY}" "${XDG_RUNTIME_DIR}/${WAYLAND_DISPLAY}" \ --ro-bind /usr/lib/ /usr/lib/ \ --ro-bind /usr/libexec/imv-wayland /usr/libexec/imv-wayland \ --ro-bind /usr/share/X11/xkb/ /usr/share/X11/xkb/ \ --ro-bind "${@:-./}" "$(realpath "${@:-./}")" \ /usr/libexec/imv-wayland "${@:-./}"

If you do use commands however, you will notice it is only showing substitute characters.

Tip: Commands in imv can be entered by pressing :.

If you try to use a command it will say:

Fontconfig error: Cannot load default config file: No such file: (null)

Look at the fonts-conf manpage (which is from fontconfig-doc) we see that /etc/fonts/ is the system font configuration directory and "${XDG_CONFIG_HOME}/fontconfig/" is the per-user configuration directory. Add "${XDG_CONFIG_HOME}/fontconfig/" with --ro-bind-try so it doesn't have to exist:

--ro-bind /etc/fonts/ /etc/fonts/ \
--ro-bind-try "${XDG_CONFIG_HOME}/fontconfig/" "${XDG_CONFIG_HOME}/fontconfig/" \

The default directories scanned for font files are /usr/share/fonts/ and "${XDG_DATA_HOME}/fonts/". Add "${XDG_DATA_HOME}/fonts/" with --ro-bind-try:

--ro-bind /usr/share/fonts/ /usr/share/fonts/ \
--ro-bind-try "${XDG_DATA_HOME}/fonts/" "${XDG_DATA_HOME}/fonts/" \

Also need:

--setenv XDG_DATA_HOME "$XDG_DATA_HOME" \

The user cache of font information is also needed, by default "${XDG_CACHE_HOME}/fontconfig/":

--bind-try "${XDG_CACHE_HOME}/fontconfig/" "${XDG_CACHE_HOME}/fontconfig/" \
Note: It seems to still work with --ro-bind-try, does it not need to write to it?

Also need:

--setenv XDG_CACHE_HOME "$XDG_CACHE_HOME" \
--ro-bind /usr/share/icu/ /usr/share/icu/ \

Is also needed or when you do :<backspace> it will terminate the process. ICU provides Unicode and Globalization support.

Todo: This was found by: Bubblewrap#Can't_find_what_path_is_missing, any better way?


The updated ~/.local/bin/bwrap-imv should look like this:

Contents of ~/.local/bin/bwrap-imv

#!/usr/bin/env sh # imv wrapped in bwrap. set -u XDG_CONFIG_HOME="${XDG_CONFIG_HOME:=$HOME/.config}" /usr/bin/bwrap \ --unshare-all \ --new-session \ --die-with-parent \ --clearenv \ --setenv WAYLAND_DISPLAY "$WAYLAND_DISPLAY" \ --setenv XDG_CACHE_HOME "$XDG_CACHE_HOME" \ --setenv XDG_CONFIG_HOME "$XDG_CONFIG_HOME" \ --setenv XDG_DATA_HOME "$XDG_DATA_HOME" \ --setenv XDG_RUNTIME_DIR "$XDG_RUNTIME_DIR" \ --ro-bind /bin/sh /bin/sh \ --dev-bind /dev/dri/renderD128 /dev/dri/renderD128 \ --dev-bind /dev/null /dev/null \ --bind-try "${XDG_CACHE_HOME}/fontconfig/" "${XDG_CACHE_HOME}/fontconfig/" \ --ro-bind-try "${XDG_CONFIG_HOME}/fontconfig/" "${XDG_CONFIG_HOME}/fontconfig/" \ --ro-bind-try "${XDG_CONFIG_HOME}/imv/config" "${XDG_CONFIG_HOME}/imv/config" \ --ro-bind-try "${XDG_DATA_HOME}/fonts/" "${XDG_DATA_HOME}/fonts/" \ --ro-bind /etc/fonts/ /etc/fonts/ \ --ro-bind /lib/ /lib/ \ --ro-bind /sys/dev/char/ /sys/dev/char/ \ --ro-bind /sys/devices/pci0000:00/ /sys/devices/pci0000:00/ \ --ro-bind "${XDG_RUNTIME_DIR}/${WAYLAND_DISPLAY}" "${XDG_RUNTIME_DIR}/${WAYLAND_DISPLAY}" \ --ro-bind /usr/lib/ /usr/lib/ \ --ro-bind /usr/libexec/imv-wayland /usr/libexec/imv-wayland \ --ro-bind /usr/share/X11/xkb/ /usr/share/X11/xkb/ \ --ro-bind /usr/share/fonts/ /usr/share/fonts/ \ --ro-bind /usr/share/icu/ /usr/share/icu/ \ --ro-bind "${@:-./}" "$(realpath "${@:-./}")" \ /usr/libexec/imv-wayland "${@:-./}"

Finally test what all is allowed by replacing /usr/libexec/imv-wayland "${@:-./}") with /bin/sh and adding --ro-bind /bin/ /bin/ \. Check around and see what the filesystem is like:

Contents of ~/.local/bin/bwrap-imv

... --ro-bind "${@:-./}" "$(realpath "${@:-./}")" \ --ro-bind /bin/ /bin/ \ /bin/sh)

Invoke bwrap-imv:

$ bwrap-imv

Show what environment variables are active:

$ printenv

See what directories are at root:

$ ls -la / ... bin ... dev ... etc ... home ... lib ... sys ... tmp ... usr

exit when done:

$ exit

Do not forget to change it back:

Contents of ~/.local/bin/bwrap-imv

... --ro-bind "${@:-./}" "$(realpath "${@:-./}")" \ /usr/libexec/imv-wayland "${@:-./}")

All done with a basic bubblewrap wrapper.

Seccomp

This material needs expanding ...

.desktop integration

This material is obsolete ...

This should probably be documented in Default_applications and linked here. Nothing is unique in using with bwrap. (Discuss)

Note: This section is also using imv as the example.

XDG Desktop Entry Specification are a set of standards describing how a particular program is to be launched, how it appears in menus, etc.

The default .desktop file for imv is at /usr/share/applications/imv.desktop. Move it to "${XDG_DATA_HOME}/applications/bwrap-imv.desktop".

Only 3 options will need to be changed: Name/Name[en_US], what shows up in the application menu in a graphical file manager (if you have one installed); Exec, program to execute:

Contents of "${XDG_DATA_HOME}/applications/bwrap-imv.desktop"

... Name=bwrap-imv Name[en_US]=bwrap-imv Exec=bwrap-imv %F ...

The program xdg-open (from the xdg-utils package) can be used to open files based on the MIME type + corresponding entry in "${XDG_CONFIG_HOME}/mimeapps.list" and "${XDG_DATA_HOME}/applications/mimeinfo.cache".

Install desktop-file-utils if it is not installed already, it comes with two commands that are needed desktop-file-validate and update-desktop-database:

# apk add desktop-file-utils

Validate

It is a good idea to validate imv.desktop using desktop-file-validate:

$ desktop-file-validate "${XDG_DATA_HOME}/applications/bwrap-imv.desktop"

Update database

This will make entries in "${XDG_DATA_HOME}/applications/" take precedence over system-wide files (/usr/share/applications/). However "${XDG_CONFIG_HOME}/mimeapps.list" has precedence over both.

Updating the database, will create "${XDG_DATA_HOME}/applications/mimeinfo.cache":

$ update-desktop-database "${XDG_DATA_HOME}/applications"

Troubleshooting

Can't find what path is missing

If all else fails start broad and work toward narrowing. See if bwrap works with the program at all:

$ bwrap \ --dev-bind / / PROGRAM

If that works start to narrow:

$ bwrap \ --ro-bind /bin/ /bin/ \ --dev-bind /dev/ /dev/ \ --ro-bind /lib/ /lib/ \ --ro-bind /sys/ /sys/ \ --ro-bind /usr/ /usr/ \ PROGRAM

Keep going till you have narrowed as much as possible.

See also