-
-
Save sloonz/4b7f5f575a96b6fe338534dbc2480a5d to your computer and use it in GitHub Desktop.
#!/usr/bin/python | |
import argparse | |
import os | |
import shlex | |
import sys | |
import tempfile | |
import yaml | |
config = yaml.full_load(open(os.path.expanduser("~/.config/sandbox.yml"))) | |
parser = argparse.ArgumentParser() | |
parser.add_argument("--name", "-n", action="store") | |
parser.add_argument("--preset", "-p", nargs=1, action="append") | |
parser.add_argument("--as", "-a", action="store") | |
bwrap_args0 = ("unshare-all", "share-net", "unshare-user", "unshare-user-try", "unshare-ipc", "unshare-net", "unshare-uts", "unshare-cgroup", "unshare-cgroup-try", "clearenv", "new-session", "die-with-parent", "as-pid-1") | |
bwrap_args1 = ("args", "userns", "userns2", "pidns", "uid", "gid", "hostname", "chdir", "unsetenv", "lock-file", "sync-fd", "remount-ro", "exec-label", "file-label", "proc", "dev", "tmpfs", "mqueue", "dir", "seccomp", "add-seccomp-fd", "block-fd", "userns-block-fd", "json-status-fd", "cap-add", "cap-drop", "perms") | |
bwrap_args2 = ("setenv", "bind", "bind-try", "dev-bind", "dev-bind-try", "ro-bind", "ro-bind-try", "file", "bind-data", "ro-bind-data", "symlink", "chmod") | |
for a in bwrap_args0: | |
parser.add_argument("--" + a, action="store_true") | |
for a in bwrap_args1: | |
parser.add_argument("--" + a, nargs=1, action="append") | |
for a in bwrap_args2: | |
parser.add_argument("--" + a, nargs=2, action="append") | |
parser.add_argument("command", nargs="+") | |
args = parser.parse_args() | |
bwrap_command = ["bwrap"] | |
system_bus_args = set() | |
session_bus_args = set() | |
executable = getattr(args, "as") or args.command[0] | |
executable = executable.split("/")[-1] | |
def expand(s, extra_env): | |
return str(s).format(env={**os.environ, **extra_env}, command=args.command, executable=executable, pid=os.getpid()) | |
def handle_bind(params, create, typ, extra_env): | |
if isinstance(params, str): | |
params = [params, params] | |
src, dst = params | |
src = expand(src, extra_env) | |
dst = expand(dst, extra_env) | |
if create: | |
os.makedirs(src, exist_ok=True) | |
return ("--" + typ, src, dst) | |
def handle_setup(config, setup, extra_env): | |
setup = setup.copy() | |
setup_args = [] | |
use_params = setup.pop("use", None) | |
if use_params: | |
for preset in use_params: | |
for preset_setup in config["presets"][preset]: | |
setup_args.extend(handle_setup(config, preset_setup, extra_env)) | |
args_params = setup.pop("args", None) | |
if args_params: | |
setup_args.extend(expand(a, extra_env) for a in args_params) | |
setenv_params = setup.pop("setenv", None) | |
if isinstance(setenv_params, dict): | |
for k, v in setenv_params.items(): | |
extra_env[k] = expand(v, extra_env) | |
setup_args.extend(("--setenv", k, extra_env[k])) | |
elif isinstance(setenv_params, list): | |
for k in setenv_params: | |
if k in os.environ: | |
setup_args.extend(("--setenv", k, os.environ[k])) | |
for bind_type in ("bind", "bind-try", "dev-bind", "dev-bind-try", "ro-bind", "ro-bind-try"): | |
bind_params = setup.pop(bind_type, None) | |
if bind_params: | |
setup_args.extend(handle_bind(bind_params, setup.pop("bind-create", None), bind_type, extra_env)) | |
for dbus_setup in ("see", "talk", "own", "call", "broadcast"): | |
dbus_setup_params = setup.pop("dbus-" + dbus_setup, None) | |
if dbus_setup_params: | |
is_system = setup.pop("system-bus", False) | |
if is_system: | |
system_bus_args.add("--%s=%s" % (dbus_setup, dbus_setup_params)) | |
else: | |
session_bus_args.add("--%s=%s" % (dbus_setup, dbus_setup_params)) | |
file_params = setup.pop("file", None) | |
if file_params: | |
data, dst = file_params | |
pr, pw = os.pipe2(0) | |
if os.fork() == 0: | |
os.close(pr) | |
os.write(pw, data.encode()) | |
sys.exit(0) | |
else: | |
os.close(pw) | |
setup_args.extend(("--file", str(pr), expand(dst, extra_env))) | |
dir_params = setup.pop("dir", None) | |
if dir_params: | |
setup_args.extend(("--dir", expand(dir_params, extra_env))) | |
bind_args_params = setup.pop("bind-args", None) | |
if bind_args_params: | |
added_paths = set() | |
strict = setup.pop("strict", True) | |
ro = setup.pop("ro", True) | |
for a in args.command[1:]: | |
if os.path.exists(a): | |
path = os.path.abspath(a) | |
if not strict: | |
path = os.path.dirname(path) | |
if not path in added_paths: | |
setup_args.extend((ro and "--ro-bind" or "--bind", path, path)) | |
added_paths.add(path) | |
cwd = os.getcwd() | |
bind_cwd_params = setup.pop("bind-cwd", None) | |
if bind_cwd_params is not None: | |
ro = setup.pop("ro", False) | |
setup_args.extend((ro and "--ro-bind" or "--bind", cwd, cwd)) | |
cwd_params = setup.pop("cwd", None) | |
if cwd_params is not None: | |
if type(cwd_params) == "str": | |
setup_args.extend(("--chdir", expand(cwd_params, extra_env))) | |
elif cwd_params: | |
setup_args.extend(("--chdir", cwd)) | |
if setup.pop("restrict-tty", None): | |
# --new-session breaks interactive sessions, this is an alternative way of fixing CVE-2017-5226 | |
import seccomp | |
import termios | |
f = seccomp.SyscallFilter(defaction=seccomp.ALLOW) | |
f.add_rule(seccomp.KILL_PROCESS, "ioctl", seccomp.Arg(1, seccomp.MASKED_EQ, 0xffffffff, termios.TIOCSTI)) | |
f.add_rule(seccomp.KILL_PROCESS, "ioctl", seccomp.Arg(1, seccomp.MASKED_EQ, 0xffffffff, termios.TIOCLINUX)) | |
f.load() | |
if len(setup) != 0: | |
print("unknown setup actions: %s" % list(setup.keys())) | |
sys.exit(1) | |
return setup_args | |
def exec_bwrap(rule): | |
extra_env = {} | |
for setup in rule.get("setup", []): | |
bwrap_command.extend(handle_setup(config, setup, extra_env)) | |
for (preset,) in args.preset or []: | |
for preset_setup in config["presets"][preset]: | |
bwrap_command.extend(handle_setup(config, preset_setup, extra_env)) | |
for a in bwrap_args0: | |
if getattr(args, a.replace("-", "_")): | |
bwrap_command.append("--" + a) | |
for a in bwrap_args1: | |
for (val,) in getattr(args, a.replace("-", "_")) or []: | |
bwrap_command.extend(("--" + a, val)) | |
for a in bwrap_args2: | |
for (v1, v2) in getattr(args, a.replace("-", "_")) or []: | |
bwrap_command.extend(("--" + a, v1, v2)) | |
dbus_proxy_args = [] | |
dbus_proxy_dir = f"{os.environ['XDG_RUNTIME_DIR']}/xdg-dbus-proxy" | |
if session_bus_args or system_bus_args: | |
os.makedirs(dbus_proxy_dir, exist_ok=True) | |
if session_bus_args: | |
proxy_socket = tempfile.mktemp(prefix="session-", dir=dbus_proxy_dir) | |
dbus_proxy_args.extend((os.environ["DBUS_SESSION_BUS_ADDRESS"], proxy_socket)) | |
dbus_proxy_args.append("--filter") | |
dbus_proxy_args.extend(session_bus_args) | |
bwrap_command.extend(("--bind", proxy_socket, os.environ["DBUS_SESSION_BUS_ADDRESS"].removeprefix("unix:path="), | |
"--setenv", "DBUS_SESSION_BUS_ADDRESS", os.environ["DBUS_SESSION_BUS_ADDRESS"])) | |
if system_bus_args: | |
proxy_socket = tempfile.mktemp(prefix="system-", dir=dbus_proxy_dir) | |
dbus_proxy_args.extend(("/run/dbus/system_bus_socket", proxy_socket)) | |
dbus_proxy_args.append("--filter") | |
dbus_proxy_args.extend(system_bus_args) | |
bwrap_command.extend(("--bind", "/run/dbus/system_bus_socket", "/run/dbus/system_bus_socket")) | |
if dbus_proxy_args: | |
pr, pw = os.pipe2(0) | |
if os.fork() == 0: | |
os.close(pr) | |
dbus_proxy_command = ["xdg-dbus-proxy", "--fd=%d" % pw] + list(dbus_proxy_args) | |
os.execlp(dbus_proxy_command[0], *dbus_proxy_command) | |
# I would like to use bwrap's --block-fd, but bwrap setups then wait, and therefore may try to bind an non-existent socket | |
assert os.read(pr, 1) == b"x" # wait for xdg-dbus-proxy to be ready | |
bwrap_command.extend(("--sync-fd", str(pr))) | |
bwrap_command.extend(args.command) | |
if os.getenv("SANDBOX_DEBUG") == "1": | |
print(bwrap_command, file=sys.stderr) | |
os.execvp(bwrap_command[0], bwrap_command) | |
for rule in config["rules"]: | |
is_match = False | |
assert not (set(rule.keys()) - {"match", "no-sandbox", "setup"}) | |
if "match" in rule: | |
assert not (set(rule["match"].keys()) - {"bin", "name"}) | |
if executable and rule["match"].get("bin") == executable: | |
is_match = True | |
if args.name and rule["match"].get("name") == args.name: | |
is_match = True | |
else: | |
is_match = True | |
if is_match: | |
if rule.get("no-sandbox"): | |
os.execvp(args.command[0], args.command) | |
else: | |
exec_bwrap(rule) | |
break |
presets: | |
common: | |
- args: [--clearenv, --unshare-pid, --die-with-parent, --proc, /proc, --dev, /dev, --tmpfs, /tmp, --new-session] | |
- setenv: [PATH, LANG, XDG_RUNTIME_DIR, XDG_SESSION_TYPE, TERM, HOME, LOGNAME, USER] | |
- ro-bind: /etc | |
- ro-bind: /usr | |
- args: [--symlink, usr/bin, /bin, --symlink, usr/bin, /sbin, --symlink, usr/lib, /lib, --symlink, usr/lib, /lib64, --tmpfs, "{env[XDG_RUNTIME_DIR]}"] | |
- bind: /run/systemd/resolve | |
private-home: | |
- bind: ["{env[HOME]}/sandboxes/{executable}/", "{env[HOME]}"] | |
bind-create: true | |
- dir: "{env[HOME]}/.config" | |
- dir: "{env[HOME]}/.cache" | |
- dir: "{env[HOME]}/.local/share" | |
x11: | |
- setenv: [DISPLAY] | |
- ro-bind: /tmp/.X11-unix/ | |
wayland: | |
- setenv: [WAYLAND_DISPLAY] | |
- ro-bind: "{env[XDG_RUNTIME_DIR]}/{env[WAYLAND_DISPLAY]}" | |
pulseaudio: | |
- ro-bind: "{env[XDG_RUNTIME_DIR]}/pulse/native" | |
- ro-bind-try: "{env[HOME]}/.config/pulse/cookie" | |
- ro-bind-try: "{env[XDG_RUNTIME_DIR]}/pipewire-0" | |
drm: | |
- dev-bind: /dev/dri | |
- ro-bind: /sys | |
portal: | |
- file: ["", "{env[XDG_RUNTIME_DIR]}/flatpak-info"] | |
- file: ["", "/.flatpak-info"] | |
- dbus-call: "org.freedesktop.portal.*=*" | |
- dbus-broadcast: "org.freedesktop.portal.*=@/org/freedesktop/portal/*" | |
rules: | |
- match: | |
bin: firefox | |
setup: | |
- setenv: | |
MOZ_ENABLE_WAYLAND: 1 | |
- use: [common, private-home, wayland, portal] | |
- dbus-own: org.mozilla.firefox.* | |
- bind: "{env[HOME]}/Downloads" | |
- bind: ["{env[HOME]}/.config/mozilla", "{env[HOME]}/.mozilla"] | |
- match: | |
name: shell | |
setup: | |
- use: [common, private-home] | |
- match: | |
bin: node | |
setup: | |
- use: [common, private-home] | |
- bind-cwd: {} | |
- cwd: true | |
- match: | |
bin: npx | |
setup: | |
- use: [common, private-home] | |
- bind-cwd: {} | |
- cwd: true | |
- match: | |
bin: npm | |
setup: | |
- use: [common, private-home] | |
- bind-cwd: {} | |
- cwd: true | |
- match: | |
name: none | |
# Fallback: anything else fall backs to a sandboxed empty home | |
- setup: | |
- use: [common, private-home, x11, wayland, pulseaudio, portal] |
@jcoffland Cool! If you want, let us know if you got it working. Btw, the blog author has his complete script here: https://github.com/StandingPadAnimations/sandboxing-scripts/blob/main/vivaldi
It works. See my updated code. https://github.com/CauldronDevelopmentLLC/sandbubble/blob/main/sbx#L334
One small problem with this method is that it shares the whole run/user/$UID/doc
with every sandbox. That exposes any files opened by one sandbox to any other running sandboxes. It would probably be better to work out how to get xdg-document-portal
to use the run/user/$UID/doc/by-app/<app-id>
directory and mount that to run/user/$UID/doc
instead.
Now I've implemented the isolation part too.
Nice! 👍
There must be something to achieve that separation, otherwise it would defeat the idea of portals and sandboxing. Won't have time to dig into that right now, but I think this should be documented somewhere...
Edit: I see you got it working already. :-)
If you are getting some dbus errors about key files, know that just having an empty /flatpak-info
is not enough.
I was trying to sandbox firefox and it complained about a missing Instance field. Looking into the flatpak-info of the flatpak version shows something like this
[Instance]
instance-id=3159835439
instance-path=/home/roger/.var/app/org.mozilla.firefox
...
arch=x86_64
flatpak-version=1.16.0
session-bus-proxy=true
system-bus-proxy=true
devel=true
extra-args=--usb-list=;--usb-list=;
I recreated some of those fields and it seems that you also need to create $XDG_RUNTIME_DIR/.flatpak/$instance-id/bwrapinfo.json
with yet more stuff. An example file looks like this
{
"child-pid": 1501,
"ipc-namespace": 4026532793,
"mnt-namespace": 4026532736,
"net-namespace": 4026532795,
"pid-namespace": 4026532738
}
I haven't tried implementing that part so far, but hopefully everything will work afterwards.
Thanks. It is a shame that xdg-dbus-proxy
is under control of flatpak.
My latest commit to https://github.com/CauldronDevelopmentLLC/sandbubble writes a minimal bwrapinfo.json
. I ran into the same problem after a recent upgrade.
I looked at the source for xdg-dbus-proxy
. It doesn't seem that complex. I was thinking of rewriting it in Python. Also, it would be nice if the proxy supported popup dialogs that asked the user if they wanted to grant various permissions to the app when the app requested them. Like Android does. I'm not sure if this is feasible. A potential problem is the dbus request timing out before the user can respond.
My latest commit to https://github.com/CauldronDevelopmentLLC/sandbubble writes a minimal
bwrapinfo.json
. I ran into the same problem after a recent upgrade.
You can get the full bwrapinfo from bwrap. Just write the output of --info-fd
to a file in the correct location.
I looked at the source for
xdg-dbus-proxy
. It doesn't seem that complex. I was thinking of rewriting it in Python. Also, it would be nice if the proxy supported popup dialogs that asked the user if they wanted to grant various permissions to the app when the app requested them. Like Android does. I'm not sure if this is feasible. A potential problem is the dbus request timing out before the user can respond.
I don't know the specifics of dbus but that sounds like a cool idea to try out.
I've followed your advice. My latest commit uses bwrap
to generate bwrapinfo.json
.
@hashkool That looks like the solution. Thank you! I'll implement this in https://github.com/CauldronDevelopmentLLC/sandbubble