1 import sys, os, glob, random, string, subprocess
2 from pprint import pprint
4 HOME = os.environ["HOME"]
5 XDG_RUNTIME_DIR = os.environ["XDG_RUNTIME_DIR"]
6 BUBBLEBOX_DIR = XDG_RUNTIME_DIR + "/bubblebox"
7 os.makedirs(BUBBLEBOX_DIR, exist_ok=True)
10 # choose from all lowercase letter
11 letters = string.ascii_lowercase
12 return ''.join(random.choice(letters) for i in range(8))
14 class BwrapInvocation:
15 """Gathered information for a bwrap invocation.
16 This will be created empty, and then each directive's `setup` function is called
17 with this object, so they can accumulate the bwrap flags and any other relevant state."""
19 # The flags to pass to bwrap.
21 # Functions to call at the end of the setup process.
22 # They will receive this object as argument, so they can add further flags.
24 # If this is `None` it means so far no d-bus proxy has been set up.
25 self.dbus_proxy_flags = None
28 """Directive that just passes flags to bwrap."""
29 def __init__(self, bwrap_flags):
30 self.bwrap_flags = bwrap_flags
31 def setup(self, bwrap):
32 bwrap.flags.extend(self.bwrap_flags)
35 """Directive that groups a bunch of directives to be treated as one."""
36 def __init__(self, directives):
37 self.directives = directives
38 def setup(self, bwrap):
39 for directive in self.directives:
40 directive.setup(bwrap)
42 class DbusProxyDirective:
43 """Directive that sets up a d-bus proxy and adds flags to it.
44 If the directive is used multiple times, the flags accumulate."""
45 def __init__(self, dbus_proxy_flags):
46 self.dbus_proxy_flags = dbus_proxy_flags
47 def setup(self, bwrap):
48 if bwrap.dbus_proxy_flags is None:
49 # We are the first d-bus proxy directive. Set up the flags and the finalizer.
50 bwrap.dbus_proxy_flags = []
51 bwrap.finalizers.append(DbusProxyDirective.launch_dbus_proxy)
52 # Always add the flags.
53 bwrap.dbus_proxy_flags.extend(self.dbus_proxy_flags)
54 def launch_dbus_proxy(bwrap):
55 """Finalizer that launches a d-bus proxy with the flags accumulated in `bwrap`."""
56 # For the system bus, we assume it to be at a fixed location and provide it to the sandbox at that same location.
57 # For the session bus, we tell the proxy to talk to DBUS_SESSION_BUS_ADDRESS on the host, but we always put it
58 # at `$XDG_RUNTIME_DIR/bus` in the sandbox.
59 session_bus = XDG_RUNTIME_DIR + "/bus" # how the sandbox will see the bus
60 system_bus = "/run/dbus/system_bus_socket"
61 session_bus_proxy = BUBBLEBOX_DIR + "/bus-" + randname()
62 system_bus_proxy = BUBBLEBOX_DIR + "/bus-system-" + randname()
63 # Prepare a pipe to coordinate shutdown of bwrap and the proxy
64 bwrap_end, other_end = os.pipe() # both FDs are "non-inheritable" now
65 # Invoke the debus-proxy
66 args = ["/usr/bin/xdg-dbus-proxy", "--fd="+str(other_end)]
67 args += ["unix:path="+system_bus, system_bus_proxy, "--filter"] # just block everything for the system bus
68 args += [os.environ["DBUS_SESSION_BUS_ADDRESS"], session_bus_proxy, "--filter"] + bwrap.dbus_proxy_flags
72 pass_fds = [other_end], # default is to pass only the std FDs!
74 # Wait until the proxy is ready
76 assert os.path.exists(session_bus_proxy)
77 # Make sure bwrap can access the other end of the pipe
78 os.set_inheritable(bwrap_end, True)
79 # Put this at the usual location for the bus insode the sandbox.
80 # TODO: What if DBUS_SESSION_BUS_ADDRESS says something else?
82 "--setenv", "DBUS_SESSION_BUS_ADDRESS", "unix:path="+session_bus,
83 "--bind", session_bus_proxy, session_bus,
84 "--bind", system_bus_proxy, system_bus,
85 "--sync-fd", str(bwrap_end),
88 # Constructors that should be used instead of directly mentioning the class above.
89 def bwrap_flags(*flags):
90 return BwrapDirective(flags)
91 def dbus_proxy_flags(*flags):
92 return DbusProxyDirective(flags)
93 def group(*directives):
94 return GroupDirective(directives)
96 # Run the application in the bubblebox with the given flags.
97 def bubblebox(*directives):
98 if len(sys.argv) <= 1:
99 print(f"USAGE: {sys.argv[0]} <program name> <program arguments>")
101 # Make sure `--die-with-parent` is always set.
102 directives = group(bwrap_flags("--die-with-parent"), *directives)
103 # Compute the bwrap invocation by running all the directives.
104 bwrap = BwrapInvocation()
105 directives.setup(bwrap)
106 for finalizer in bwrap.finalizers:
109 args = ["/usr/bin/bwrap"] + bwrap.flags + ["--"] + sys.argv[1:]
111 os.execvp(args[0], args)
113 # Give all instances of the same box a shared XDG_RUNTIME_DIR
114 def shared_runtime_dir(boxname):
115 dirname = BUBBLEBOX_DIR + "/" + boxname
116 os.makedirs(dirname, exist_ok=True)
117 return bwrap_flags("--bind", dirname, XDG_RUNTIME_DIR)
119 # Convenient way to declare host access
126 if val == Access.Read:
128 elif val == Access.Write:
130 elif val == Access.Device:
133 raise Exception(f"invalid access value: {val}")
134 def host_access(dirs):
135 def expand(root, names):
136 """`names` is one or more strings that can contain globs. Expand them all relative to `root`."""
137 if isinstance(names, str):
139 assert isinstance(names, tuple)
141 assert not (name.startswith("../") or name.__contains__("/../") or name.endswith("../"))
142 path = root + "/" + name
144 path = path.replace("//", "/")
145 path = path.removesuffix("/.")
147 globbed = glob.glob(path)
148 if len(globbed) == 0:
149 raise Exception(f"Path does not exist: {path}")
151 def recursive_host_access(root, dirs, out):
152 for names, desc in dirs.items():
153 for path in expand(root, names):
154 if isinstance(desc, dict):
155 # Recurse into children
156 recursive_host_access(path, desc, out)
158 # Allow access to this path
159 out.extend((Access.flag(desc), path, path))
160 # Start the recursive traversal
162 recursive_host_access("", dirs, out)
164 return bwrap_flags(*out)
165 def home_access(dirs):
166 return host_access({ HOME: dirs })
168 # Profile the profiles when importing bubblebox.