Created
January 8, 2026 16:43
-
-
Save sloonz/ef282a1f53366e1ed6f5cb848de015ba to your computer and use it in GitHub Desktop.
Sandboxing wrapper, merge variant. Context : https://gist.github.com/sloonz/4b7f5f575a96b6fe338534dbc2480a5d?permalink_comment_id=5926910#gistcomment-5926910
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: base | |
| clearenv: true | |
| dieWithParent: true | |
| unsharePid: true | |
| newSession: true | |
| mounts: | |
| /proc: proc | |
| /dev: dev | |
| /tmp: tmpfs | |
| /etc: {bind: {path: /etc, readOnly: true}} | |
| /usr: {bind: {path: /usr, readOnly: true}} | |
| /bin: {symlink: usr/bin} | |
| /sbin: {symlink: usr/sbin} | |
| /lib: {symlink: usr/lib} | |
| /lib64: {symlink: usr/lib} | |
| "~": tmpfs | |
| "~/.config": dir | |
| "~/.cache": dir | |
| "~/.local/share": dir | |
| "{env[XDG_RUNTIME_DIR]}": {tmpfs: {perms: 700}} | |
| /run/systemd/resolve: {bind: {}} | |
| env: | |
| PATH: true | |
| LANG: true | |
| XDG_RUNTIME_DIR: true | |
| XDG_SESSION_TYPE: true | |
| TERM: true | |
| HOME: true | |
| LOGNAME: true | |
| USER: true | |
| # CVE-2017-5226 prevention | |
| # 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.export_bpf(fd) | |
| seccomp: | |
| - arch: x86_64 | |
| data: |- | |
| IAAAAAQAAAAVAAAMPgAAwCAAAAAAAAAANQAAAQAAAEAVAAAJ/////xUAAAYQAAAAIAAAABwAAABU | |
| AAAAAAAAABUAAAMAAAAAIAAAABgAAAAVAAIAHFQAABUAAQASVAAABgAAAAAA/38GAAAAAAAAgAYA | |
| AAAAAAAA | |
| vars: | |
| flatpakInfo: |- | |
| [Application] | |
| name={name} | |
| [Instance] | |
| instance-id=bwrap-{pid} | |
| --- | |
| name: shell | |
| newSession: false | |
| env: | |
| LS_COLORS: true | |
| WORDCHARS: true | |
| EDITOR: true | |
| PYTHONIOENCODING: true | |
| PROMPT: true | |
| RPROMPT: true | |
| HISTFILE: true | |
| HISTSIZE: true | |
| SAVEHIST: true | |
| NODE_REPL_HISTORY: true | |
| SQLITE_HISTORY: true | |
| PYTHON_HISTORY: true | |
| MYSQL_HISTFILE: true | |
| mounts: | |
| "~/.zshrc": {bind: {readOnly: true}} | |
| "~/.config/git": {bind: {readOnly: true}} | |
| "~/.config/nvim": {bind: {readOnly: true}} | |
| --- | |
| name: display | |
| env: | |
| XKB_DEFAULT_LAYOUT: custom | |
| mounts: | |
| "~/.cache/mesa_shader_cache": {bind: {create: true}} | |
| "~/.cache/fontconfig": {bind: {create: true}} | |
| "~/.cache/radv_builtin_shaders": {bind: {create: true}} | |
| "~/.xkb": {bind: {readOnly: true}} | |
| --- | |
| name: x11 | |
| include: [display] | |
| env: | |
| DISPLAY: true | |
| mounts: | |
| /tmp/.X11-unix/X0: {bind: {readOnly: true}} | |
| --- | |
| name: wayland | |
| include: [display] | |
| env: | |
| WAYLAND_DISPLAY: true | |
| mounts: | |
| "{env[XDG_RUNTIME_DIR]}/{env[WAYLAND_DISPLAY]}": {bind: {readOnly: true}} | |
| --- | |
| name: audio | |
| mounts: | |
| "{env[XDG_RUNTIME_DIR]}/pulse/native": {bind: {readOnly: true}} | |
| "~/.config/pulse/cookie": {bind: {readOnly: true, try: true}} | |
| "{env[XDG_RUNTIME_DIR]}/pipewire-0": {bind: {readOnly: true}} | |
| --- | |
| name: drm | |
| mounts: | |
| /dev/dri: {bind: {dev: true}} | |
| /sys: {bind: {readOnly: true}} | |
| --- | |
| name: portal | |
| mounts: | |
| "{env[XDG_RUNTIME_DIR]}/doc": {bind: {path: "{env[XDG_RUNTIME_DIR]}/doc/by-app/{name}", create: true}} | |
| /.flatpak-info: {data: {content: "{flatpakInfo}", readOnly: true}} | |
| "{env[XDG_RUNTIME_DIR]}/flatpak-info": {data: {content: "{flatpakInfo}", readOnly: true}} | |
| dbus: | |
| sandbox: | |
| include: [base] | |
| asPid1: true | |
| mounts: | |
| /.flatpak-info: {data: {content: "{flatpakInfo}", readOnly: true}} | |
| "{env[XDG_RUNTIME_DIR]}/flatpak-info": {data: {content: "{flatpakInfo}", readOnly: true}} | |
| user: | |
| org.kde.*: own | |
| org.freedesktop.portal.*: talk | |
| org.freedesktop.Notifications: talk | |
| --- | |
| name: project | |
| include: [base, x11, wayland, rust, shell] | |
| chdir: "{env[HOME]}/workspace" | |
| env: | |
| VSCODE_EXTENSIONS: true | |
| vars: | |
| name: "{project}" | |
| projectRoot: "/opt/data/projects/{project}" | |
| mounts: | |
| "~": {bind: {path: "{projectRoot}/home", create: true}} | |
| "~/workspace": {bind: {path: "{projectRoot}/workspace", create: true}} | |
| "~/.cache": {bind: {path: "{projectRoot}/cache", create: true}} | |
| "{env[VSCODE_EXTENSIONS]}": {bind: {readOnly: true}} | |
| --- | |
| name: private-home | |
| include: [base] | |
| mounts: | |
| "~": {bind: {path: "~/.local/share/sandboxes/{name}", create: true}} | |
| "~/.config": {bind: {path: "~/.config/sandboxes/{name}", create: true}} | |
| "~/.cache": {bind: {path: "~/.cache/sandboxes/{name}", create: true}} | |
| --- | |
| name: cli | |
| include: [private-home, shell] | |
| matches: [node, npx, npm, yarn, go, tsc, cargo, rustup] | |
| vars: | |
| name: cli | |
| chdir: "{cwd}" | |
| mounts: | |
| "{cwd}": {bind: {}} | |
| --- | |
| name: none | |
| matches: [discor, gedit, foot, bnet, swtor, firefox, dolphin] | |
| disableSandbox: true | |
| --- | |
| name: default | |
| include: [private-home, x11, wayland, audio] |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/python | |
| import argparse | |
| import base64 | |
| import json | |
| import logging | |
| import os | |
| import pathlib | |
| import platform | |
| import pprint | |
| import re | |
| import shlex | |
| import sys | |
| import yaml | |
| sandboxes_cache = {} | |
| global_sandboxes = {} | |
| bwrap_flags = {"levelPrefix", "unshareAll", "shareNet", "unshareUser", "unshareUserTry", "unshareIpc", "unsharePid", "unshareNet", "unshareUts", "unshareCgroup", "unshareCgroupTry", "disableUserns", "assertUsernsDisabled", "clearenv", "newSession", "dieWithParent", "asPid1"} | |
| bwrap_options = {"argv0", "userns", "userns2", "pidns", "uid", "gid", "hostname", "chdir", "execLabel", "fileLabel", "seccomp", "syncFd", "blockFd", "usernsBlockFd", "infoFd", "jsonStatusFd"} | |
| bwrap_list_options = {"addSeccompFd", "capAdd", "capDrop", "lockFile", "remountRo"} | |
| merge_policy = { | |
| "items": {"mounts", "chmod"}, | |
| "literal": bwrap_flags.union(bwrap_options).union({"disableSandbox", "vars.*", "env.*", "dbus.sloppyNames.*", "dbus.sandbox.*", "dbus.user.*", "dbus.system.*"}), | |
| "list": bwrap_list_options.union({"extraArgs"}).union({"matches", "dbus.rules.*.*.*"}), | |
| "dict": {"vars", "env", "dbus", "dbus.sloppyNames", "dbus.sandbox", "dbus.user", "dbus.system", "dbus.rules.*", "dbus.rules.*.*"}, | |
| "discard": {"name", "include"}, | |
| } | |
| def tagged_append(tag, dest): | |
| class TaggedAppend(argparse.Action): | |
| def __call__(self, parser, ns, values, option_strings=None): | |
| dest.append((tag, values)) | |
| return TaggedAppend | |
| def load_sandboxes_file(path, default_name=None): | |
| logging.debug("loading %s", path) | |
| with open(path) as fd: | |
| sandboxes = list(yaml.safe_load_all(fd)) | |
| sandboxes = [sandboxes] if isinstance(sandboxes, dict) else sandboxes | |
| for i, sb in enumerate(sandboxes): | |
| if name := sb.get("name", default_name if i == 0 else None): | |
| sandboxes_cache[name] = sb | |
| return sandboxes | |
| def try_load_sandbox(name): | |
| if name in sandboxes_cache: | |
| return sandboxes_cache[name] | |
| if name in global_sandboxes: | |
| return global_sandboxes[name] | |
| candidate_paths = [ | |
| pathlib.Path(name), | |
| pathlib.Path(f"{name}.yml"), | |
| pathlib.Path(f"{name}.yaml"), | |
| pathlib.Path.home() / ".config" / "sandbox" / f"{name}.yml", | |
| pathlib.Path.home() / ".config" / "sandbox" / f"{name}.yaml", | |
| ] | |
| for p in candidate_paths: | |
| if p.exists(): | |
| return load_sandboxes_file(p, name)[0] | |
| def load_sandbox(name): | |
| sb = try_load_sandbox(name) | |
| if sb is None: | |
| raise Exception(f"sandbox not found: {name}") | |
| return sb | |
| def get_merge_policy(path, k): | |
| kl = ".".join(path + [k]) | |
| kg = ".".join(path + ["*"]) | |
| for p, s in merge_policy.items(): | |
| if kl in s: | |
| return p, path + [k] | |
| if kg in s: | |
| return p, path + ["*"] | |
| raise Exception(f"Unknown key while merging: {".".join(path + [k])}") | |
| def merge(a, b, path=[]): | |
| res = {} | |
| for k in set(a.keys()).union(b.keys()): | |
| policy, key_path = get_merge_policy(path, k) | |
| if policy == "list": | |
| res[k] = a.get(k, []) + b.get(k, []) | |
| elif policy == "items": | |
| left = a.get(k, {}) | |
| right = b.get(k, {}) | |
| res[k] = (list(left.items()) if isinstance(left, dict) else left) + \ | |
| (list(right.items()) if isinstance(right, dict) else right) | |
| elif policy == "dict": | |
| res[k] = merge(a.get(k, {}), b.get(k, {}), key_path) | |
| elif policy == "literal": | |
| res[k] = b.get(k, a.get(k, None)) | |
| return res | |
| def merge_sandboxes(sandboxes): | |
| def load_include(inc): | |
| inc = {"name": inc} if isinstance(inc, str) else inc | |
| if name := inc.get("name"): | |
| if inc.get("try"): | |
| return try_load_sandbox(name) | |
| else: | |
| return load_sandbox(name) | |
| elif path := inc.get("path"): | |
| if inc.get("try") and not os.path.exists(path): | |
| return {} | |
| else: | |
| return load_sandboxes_file(path)[0] | |
| else: | |
| assert False | |
| res = {} | |
| for sb in sandboxes: | |
| inc = merge_sandboxes(load_include(child_sb) for child_sb in sb.get("include", [])) | |
| res = merge(res, merge(inc, sb)) | |
| return res | |
| def get_sandbox(sb, default_vars): | |
| # Parse vars & env | |
| raw_vars = {**sb.get("vars", {})} | |
| raw_env = {} | |
| vars = {**default_vars} | |
| env = {} | |
| env_unset = set() | |
| for k, v in sb.get("env", {}).items(): | |
| if v is True: # inherit | |
| if k in os.environ: | |
| env[k] = os.environ[k] | |
| elif v is False: # clear | |
| env_unset.add(k) | |
| elif v is None: # nothing | |
| pass | |
| elif isinstance(v, str): | |
| raw_env[k] = v | |
| elif isinstance(v, dict): | |
| if "inherits" in v: | |
| if k in os.environ or "defaultValue" not in v: | |
| env[k] = os.environ[k] | |
| else: | |
| raw_env[k] = v["defaultValue"] | |
| elif "value" in v: | |
| if v.get("raw", False): | |
| env[k] = v["value"] | |
| else: | |
| raw_env[k] = v["value"] | |
| else: | |
| raise Exception(f"Invalid value for environment variable {k}: {repr(v)}") | |
| else: | |
| raise Exception(f"Invalid value for environment variable {k}: {repr(v)}") | |
| format_vars = {**vars, "env": {**os.environ, **env}} | |
| while True: | |
| changed = False | |
| for parsed, raw in ((vars, raw_vars), (env, raw_env)): | |
| for k in list(raw.keys()): | |
| try: | |
| parsed[k] = raw[k].format(**format_vars) | |
| del raw[k] | |
| changed = True | |
| except KeyError: | |
| pass | |
| format_vars = {**vars, "env": {**os.environ, **env}} | |
| if not raw_env and not raw_vars: | |
| break | |
| if not changed: | |
| assert False # circular definition | |
| # Parse mounts & chmod | |
| mounts = {os.path.expanduser(k.format(**format_vars).rstrip("/")): v for k, v in sb.get("mounts", []) if v} | |
| chmod = {os.path.expanduser(k.format(**format_vars).rstrip("/")): v for k, v in sb.get("chmod", []) if v} | |
| return {**sb, "vars": vars, "env": env, "envUnset": env_unset, "mounts": mounts, "chmod": chmod} | |
| def pipefd(data): | |
| pr, pw = os.pipe2(0) | |
| if os.fork() == 0: | |
| os.close(pr) | |
| os.write(pw, data) | |
| sys.exit(0) | |
| else: | |
| os.close(pw) | |
| return pr | |
| def get_bwrap_args(sb): | |
| def bwrap_name(name): | |
| if name == "argv0": | |
| return name | |
| return re.sub(r"(?<=[a-z])([A-Z0-9+])", lambda m: "-" + m.group(1).lower(), name) | |
| def format_seccomp_value(value): | |
| if isinstance(value, dict): # {data: string, arch: string} | |
| value = [value] | |
| if isinstance(value, list): # {data: string, arch: string}[] | |
| data = [base64.b64decode(prog["data"]) for prog in value if prog["arch"] == platform.machine()] | |
| if len(data) == 0: | |
| raise Exception(f"seccomp program not found for that architecture: {platform.machine()}") | |
| return str(pipefd(data[0])) | |
| else: # fd | |
| return str(value) | |
| def format_option_value(name, value): | |
| if name in ("seccomp", "addSeccomp"): | |
| return format_seccomp_value(value) | |
| elif name in ("userns", "userns2", "pidns","syncFd", "blockFd", "userNsBlockFd", "infoFd", "jsonStatusFd"): | |
| return str(value) | |
| else: | |
| return str(value).format(**format_vars) | |
| def format_datasource_value(value): # {fd: number} | {content: string, raw?: boolean, base64?: boolean} | |
| if (fd := value.get("fd")) is not None: | |
| return str(fd) | |
| elif (content := value.get("content")) is not None: | |
| if not value.get("raw"): | |
| content = content.format(**format_vars) | |
| if value.get("base64"): | |
| content = base64.b64decode(content) | |
| else: | |
| content = content.encode("utf-8") | |
| return str(pipefd(content)) | |
| else: | |
| raise NotImplementedError | |
| format_vars = {**sb["vars"], "env": {**os.environ, **sb["env"]}} | |
| args = [f"--{bwrap_name(f)}" for f in bwrap_flags if sb.get(f)] | |
| for o in bwrap_options: | |
| if sb.get(o) not in (False, None): | |
| args.extend((f"--{bwrap_name(o)}", format_option_value(o, sb[o]))) | |
| for o in bwrap_list_options: | |
| for v in sb.get(o, []): | |
| if v not in (False, None): | |
| args.extend((f"--{bwrap_name(o)}", format_option_value(o, v))) | |
| args.extend(arg.format(**format_vars) for arg in sb.get("extraArgs", [])) | |
| for e in sb["envUnset"]: | |
| args.extend(("--unsetenv", e)) | |
| for k, v in sb["env"].items(): | |
| args.extend(("--setenv", k, v)) | |
| for dest_path, mount in sorted(sb["mounts"].items()): | |
| if mount in ("proc", "dev", "tmpfs", "mqueue", "dir"): | |
| args.extend((f"--{mount}", dest_path.format(**format_vars))) | |
| elif isinstance(mount, dict): | |
| if (tmpfs := mount.get("tmpfs")) is not None: # { tmpfs: { perms?: number; size?: number }} | |
| if (perms := tmpfs.get("perms")) is not None: | |
| args.extend(("--perms", str(perms))) | |
| if (size := tmpfs.get("size")) is not None: | |
| args.extend(("--size", str(size))) | |
| args.extend(("--tmpfs", dest_path)) | |
| elif (dir := mount.get("dir")) is not None: # { dir: { perms?: number }} | |
| if (perms := dir.get("perms")) is not None: | |
| args.extend(("--perms", str(perms))) | |
| args.extend(("--dir", dest_path)) | |
| elif (symlink := mount.get("symlink")) is not None: # { symlink: string } | |
| args.extend(("--symlink", symlink.format(**format_vars), dest_path)) | |
| elif (bind := mount.get("bind")) is not None: # { bind: { path: string; readOnly?: boolean; dev?: boolean; try?: boolean, create?: boolean }} | |
| prefix = "dev-" if bind.get("dev") else "ro-" if bind.get("ro") else "" | |
| suffix = "-try" if bind.get("try") else "" | |
| src_path = os.path.expanduser(bind.get("path", dest_path).format(**format_vars)) | |
| if bind.get("create"): | |
| os.makedirs(src_path, exist_ok=True) | |
| args.extend(("--" + prefix + "bind" + suffix, src_path, dest_path)) | |
| elif (fd := mount.get("fd")) is not None: # { fd: { fd: number; readOnly?: boolean }} | |
| args.extend(("--ro-bind-fd" if fd.get("readOnly") else "--bind-fd", str(fd["fd"]))) | |
| elif (file := mount.get("file")) is not None: # { file: DataSource & { perms?: number }} | |
| if (perms := file.get("perms")) is not None: | |
| args.extend(("--perms", str(perms))) | |
| args.extend(("--file", format_datasource_value(file), dest_path)) | |
| elif (data := mount.get("data")) is not None: # { data: DataSource & { readOnly?: boolean; perms?: number }} | |
| if (perms := data.get("perms")) is not None: | |
| args.extend(("--perms", str(perms))) | |
| args.extend(("--ro-bind-data" if data.get("readOnly") else "--bind-data", format_datasource_value(data), dest_path)) | |
| elif (overlay := mount.get("overlay")) is not None: # { overlay: { lower: string[]; upper?: string; work?: string; mode?: "rw" | "tmp" | "ro" }} | |
| for lower in overlay["lower"]: | |
| args.extend(("--overlay-src", lower.format(**format_vars))) | |
| mode = overlay.get("mode", "rw" if "upper" in overlay and "work" in overlay else "tmp") | |
| if mode == "rw": | |
| args.extend(("--overlay", overlay["upper"], overlay["work"], dest_path)) | |
| else: | |
| args.extend((f"--{mode}-overlay", dest_path)) | |
| else: | |
| raise Exception(f"invalid mount value: {repr(mount)}") | |
| else: | |
| raise Exception(f"invalid mount value: {repr(mount)}") | |
| for path, mode in sorted(sb["chmod"]): | |
| args.extend(("--chmod", path.format(**format_vars), str(mode))) | |
| return args | |
| def get_dbus_proxy_args(sb, bus): | |
| args = [] | |
| if sb.get("dbus", {}).get("sloppyNames", {}).get(bus, False): | |
| args.append("--sloppy-names") | |
| for name, policy in sb.get("dbus", {}).get(bus, {}).items(): | |
| args.append(f"--{policy}={name}") | |
| for rule_type in ("broadcast", "call"): | |
| for name, rules in sb.get("dbus", {}).get("rules", {}).get(bus, {}).get(rule_type, {}).keys(): | |
| args.extend(f"--broadcast={name}={rule}" for rule in rules) | |
| return args | |
| def setup_dbus_proxy(sb, vars, proxy_dir, buses): | |
| args = [] | |
| proxy_bwrap_args = ["--bind", proxy_dir, proxy_dir] | |
| cmd_bwrap_args = [] | |
| for bus, address, addr_env in buses: | |
| if bus_args := get_dbus_proxy_args(sb, bus): | |
| args.extend((address, f"{proxy_dir}/{bus}", "--filter")) | |
| args.extend(bus_args) | |
| addr_path = address.removeprefix("unix:path=") | |
| proxy_bwrap_args.extend(("--bind", addr_path, addr_path)) | |
| cmd_bwrap_args.extend(("--bind", f"{proxy_dir}/{bus}", addr_path)) | |
| if addr_env: | |
| cmd_bwrap_args.extend(("--setenv", addr_env, address)) | |
| if not args: | |
| return [] | |
| pr, pw = os.pipe2(0) | |
| os.makedirs(proxy_dir, exist_ok=True) | |
| args = ["xdg-dbus-proxy", f"--fd={pw}"] + args | |
| logging.debug("proxy args for dbus proxy: %r", shlex.join(args)) | |
| if proxy_sb := sb.get("dbus", {}).get("sandbox"): | |
| proxy_sb = get_sandbox(merge_sandboxes([proxy_sb]), vars) | |
| debug_object("dbus proxy sandbox", proxy_sb) | |
| proxy_bwrap_args = get_bwrap_args(proxy_sb) + proxy_bwrap_args | |
| args = ["bwrap", "--args", str(pipefd("\0".join(bwrap_args).encode("utf-8")))] + args | |
| logging.debug("bwrap args for xdg-dbus-proxy for bus: %s", shlex.join(proxy_bwrap_args)) | |
| if os.fork() == 0: | |
| os.close(pr) | |
| os.execlp(args[0], *args) | |
| else: | |
| os.close(pw) | |
| assert os.read(pr, 1) == b"x" | |
| return ["--sync-fd", str(pr)] + cmd_bwrap_args | |
| def debug_object(label, obj): | |
| if logging.getLogger().isEnabledFor(logging.DEBUG): | |
| logging.debug("%s:\n%s", label, pprint.pformat(obj)) | |
| configs_sources = [] | |
| parser = argparse.ArgumentParser() | |
| parser.add_argument("-l", "--log-level", choices=[lvl.lower() for lvl in ("CRITICAL", "ERROR", "WARNING", "INFO", "DEBUG")]) | |
| parser.add_argument("-n", "--name", action=tagged_append("name", configs_sources)) | |
| parser.add_argument("-j", "--json", action=tagged_append("json", configs_sources)) | |
| parser.add_argument("-f", "--file", action=tagged_append("file", configs_sources)) | |
| parser.add_argument("-s", "--set", action=tagged_append("set", configs_sources)) | |
| parser.add_argument("-a", "--autoload", action="store_true") | |
| parser.add_argument("-M", "--no-match", action="store_true") | |
| parser.add_argument("-D", "--no-default", action="store_true") | |
| parser.add_argument("--default-sandbox", action="store", default="default") | |
| parser.add_argument("executable", nargs="?") | |
| parser.add_argument("args", nargs=argparse.REMAINDER) | |
| args = parser.parse_args() | |
| executable_name = os.path.basename(args.executable or os.environ.get("SHELL", "sh")) | |
| if args.log_level: | |
| logging.getLogger().setLevel(getattr(logging, args.log_level.upper())) | |
| for global_path in (pathlib.Path.home() / ".config" / "sandbox.yaml", pathlib.Path.home() / ".config" / "sandbox.yml"): | |
| if global_path.exists(): | |
| global_sandboxes = load_sandboxes_file(global_path) | |
| configs = [] | |
| for source_type, source_data in configs_sources: | |
| if source_type == "name": | |
| for name in source_data.split(","): | |
| configs.append(load_sandbox(name.strip())) | |
| elif source_type == "json": | |
| configs.append(json.loads(source_data)) | |
| elif source_type == "file": | |
| configs.extend(load_sandboxes_file(source_data)) | |
| elif source_type == "set": | |
| k, v = source_data.split("=", 1) | |
| k = re.sub(r"^\$", "env.", re.sub("^:", "vars.", k)) | |
| v = json.loads(v) if v and (v[0] in '"[{'or v in ("true", "false", "null")) else v | |
| obj = {} | |
| cur = obj | |
| for is_array, p, end in re.findall(r'(?:^|\.)(@?)("[^"=]+"|[^".=]+)(=$)?', f"{k}="): | |
| p = p[1:-1] if p.startswith('"') else p | |
| c = v if end else {} | |
| if is_array: | |
| cur[p] = [c] | |
| cur = cur[p][0] | |
| else: | |
| cur[p] = c | |
| cur = cur[p] | |
| configs.append(obj) | |
| else: | |
| raise NotImplementedError | |
| if args.autoload: | |
| sb = try_load_sandbox(executable_name) | |
| if sb and sb not in configs: | |
| configs.append(sb) | |
| if not args.no_match: | |
| for sb in global_sandboxes: | |
| if executable_name in sb.get("matches", []) and sb not in configs: | |
| configs.append(sb) | |
| if not configs and not args.no_default: | |
| configs = [load_sandbox(args.default_sandbox)] | |
| debug_object("configs", configs) | |
| default_vars = { | |
| "pid": os.getpid(), | |
| "cwd": os.getcwd(), | |
| "executable": args.executable, | |
| "name": executable_name, | |
| } | |
| sb = get_sandbox(merge_sandboxes(configs), default_vars) | |
| debug_object("sandbox", sb) | |
| if sb.get("disableSandbox"): | |
| os.execlp(args.executable, args.executable, *args.args) | |
| dbus_proxy_dir = f"{os.environ["XDG_RUNTIME_DIR"]}/xdg-dbus-proxy/bwrap-{os.getpid()}" | |
| dbus_proxy_args = setup_dbus_proxy(sb, default_vars, dbus_proxy_dir, ( | |
| ("system", "unix:path=/run/dbus/system_bus_socket", None), | |
| ("user", os.environ["DBUS_SESSION_BUS_ADDRESS"], "DBUS_SESSION_BUS_ADDRESS"), | |
| )) | |
| bwrap_args = get_bwrap_args(sb) | |
| bwrap_args.extend(dbus_proxy_args) | |
| logging.debug("bwrap command: %s", shlex.join(["bwrap"] + bwrap_args + [args.executable] + args.args)) | |
| os.execlp("bwrap", "bwrap", "--args", str(pipefd("\0".join(bwrap_args).encode("utf-8"))), args.executable or executable_name, *args.args) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Hi @sloonz !
Been following the parent gist thread and read the relevant excellent original blog posts some months ago.
My knee-jerk reaction to problems like these are always bash-first, but the more I've thought about it the solution has been converging with this very gist, so I've decided to nuke my original work and steal from you.
Copied this gist to haridusministeerium/bubblebox and have
TODOs with potential issues, questions & change ideas.(broad-strokes changes are in commit messags)
Feel free to ignore, as it's non-trivial amount of questions, but I'd appreciate if you could take the time to answer the questions below, and TODO's sprinkled across bb script itself; think latter is best answered by opening a review on the file itself.
Questions:
--fd=FDfile descriptor as opposed to via command line per usual?get_merge_policy(): what dokl&kgstand for? key-literal & key-glob?--sync-fdused?--autoloadisn't the default?--fdparam?setup_dbus_proxy(): what's withdoes that signify the proxies are ready? where's "x" payload documented?
-s/--setoption parsing magic could use some documentationused by bwrap/xdg-dbus-proxy so there's no need for translation like what
bwrap_name()does?in fact, is there even a real value in defining them as keys under sandbox config,
as opposed to having list of say 'bwrapOpts' where we set the flags to pass through?
that would also give us free validation by bwrap, as opposed to having to do
it ourselves. note there absolutely is value in mounts and envs with shorter
syntax and vars/env var expansion, but for simple opts... why overengineer?
one potential issue with just-a-list-of-opts is how these options get merged,
as we'd most likely just end up concating the lists.
it was generated and which seccomp rule it provides.
dbusconfig looks off to me. think it should be instead:which means if config only defines say
...then user proxy is created with just the '--filter' flag.
this way there's clear separation between user & system bus configurations.
why are
"dbus.rules.*.*.*"merged by 'list' merge policy? can there be e.g. multiple--broadcast=org.freedesktop.portal.*=rules? My intuition tells it should be merged by 'literal', i.e. same rule names/keys should overwrite previous ones.EDIT: points 11-12 solved by a proposed solution on
reworkbranch.is there a requirement to bind
{env[XDG_RUNTIME_DIR]}/doc/by-app/{name}for portal? At least on my debian testing installation, we don't have write perms to create the dir:EDIT: looks like it was a misunderstanding on my part. For portals to work, we need
bwrapinfo.jsonto be written via bwrap's--info-fdoption, also sorted on thereworkbranch (fix)Thanks, and especially thanks for this script!