Compare commits

1 Commits

Author SHA1 Message Date
2cb658c723 add dbus and dri 2021-06-27 16:46:29 +02:00
19 changed files with 104 additions and 762 deletions

4
.gitignore vendored
View File

@@ -1,4 +1,2 @@
.idea .idea
bwbox main
result
scripts/applications

View File

@@ -1,13 +0,0 @@
# Package
version = "1.0.0"
author = "mawalu"
description = "An experimental sandbox tool for linux apps"
license = "MIT"
srcDir = "."
bin = @["bwbox"]
# Dependencies
requires "nim >= 1.6.0"

View File

@@ -1,6 +1,6 @@
{ {
"mount": [], "mount": [],
"romount": ["/etc", "/var", "/usr", "/opt"], "romount": ["/etc", "/var", "/usr", "/opt", ".oh-my-zsh", ".zsh", ".zshrc"],
"symlinks": [ "symlinks": [
{"src": "usr/lib", "dst": "/lib"}, {"src": "usr/lib", "dst": "/lib"},
{"src": "usr/lib64", "dst": "/lib64"}, {"src": "usr/lib64", "dst": "/lib64"},

View File

@@ -1,4 +0,0 @@
{
"extends": "shell",
"mountcwd": true
}

View File

@@ -1,6 +0,0 @@
{
"extends": "shell",
"romount": [".gitconfig", ".gnupg", "/run/user/1000/gnupg", ".ssh/config"],
"mountcwd": true,
"mount": [".ssh/known_hosts"]
}

View File

@@ -1,7 +0,0 @@
{
"extends": "default",
"romount": [".Xauthority", "/tmp/.X11-unix", "/run/user/1000/pulse/native"],
"dbus": true,
"dbuscall": ["org.freedesktop.Notifications.*=@/org/freedesktop/Notifications", "org.freedesktop.portal.*=*"],
"dbusbroadcast": ["org.freedesktop.portal.*=@/org/freedesktop/portal/*"]
}

View File

@@ -1,5 +0,0 @@
{
"extends": "default",
"romount": [".oh-my-zsh", ".zsh", ".zshrc", ".zshrc-local"],
"sethostname": true
}

View File

@@ -1,7 +0,0 @@
{
"extends": "default",
"romount": ["/run/user/1000/pulse/native", "/run/user/1000/wayland-1"],
"dbus": true,
"dbuscall": ["org.freedesktop.Notifications.*=@/org/freedesktop/Notifications", "org.freedesktop.portal.*=*"],
"dbusbroadcast": ["org.freedesktop.portal.*=@/org/freedesktop/portal/*"]
}

26
flake.lock generated
View File

@@ -1,26 +0,0 @@
{
"nodes": {
"nixpkgs": {
"locked": {
"lastModified": 1720893482,
"narHash": "sha256-fGQczQ3JuvqSK3rYsJvvbE7j8BENLp8DqJH1B0uXYKg=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "94c843e8f05bac70e905c48c965ba7be79bde613",
"type": "github"
},
"original": {
"owner": "NixOS",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"nixpkgs": "nixpkgs"
}
}
},
"root": "root",
"version": 7
}

View File

@@ -1,20 +0,0 @@
{
description = "An experimental sandboxing tool for linux apps";
inputs.nixpkgs.url = github:NixOS/nixpkgs;
outputs = { self, nixpkgs }: {
packages.x86_64-linux.default =
with import nixpkgs { system = "x86_64-linux"; };
buildNimPackage {
name = "bwbox";
src = self;
nativeBuildInputs = [pkgs.makeWrapper];
postInstall = ''
wrapProgram $out/bin/bwbox \
--prefix PATH ':' ${pkgs.bubblewrap}/bin \
--prefix PATH ':' ${pkgs.xdg-dbus-proxy}/bin
'';
};
};
}

View File

@@ -1,14 +1,14 @@
import parseopt
import options import options
import os import os
type Args* = object type Args* = object
name*: Option[string] name*: Option[string]
cmd*: Option[seq[string]] cmd*: Option[string]
profile*: Option[string] profile*: Option[string]
debug*: bool
proc getCmd*(args: Args): seq[string] = proc getCmd*(args: Args): string =
return args.cmd.get(@[getEnv("SHELL", "/bin/sh")]) return args.cmd.get(getEnv("SHELL", "/bin/bash"))
proc getProfile*(args: Args): string = proc getProfile*(args: Args): string =
if args.profile.isSome: if args.profile.isSome:
@@ -16,34 +16,30 @@ proc getProfile*(args: Args): string =
return "default" return "default"
proc parseOpt(args: var Args, key: string, value: string): bool =
case key
of "command", "c":
args.cmd = some(value)
of "profile", "p":
args.profile = some(value)
else:
return false
return true
proc parseArgs*(): Option[Args] = proc parseArgs*(): Option[Args] =
var args = Args(debug: false) var p = initOptParser()
var args = Args()
var command = newSeq[string]() while true:
var parsingSandboxArgs = true p.next()
var i = 1 case p.kind
of cmdEnd: break
of cmdShortOption, cmdLongOption:
if p.val == "" or args.parseOpt(p.key, p.val) == false:
echo "Invalid argument ", p.val
return
of cmdArgument:
args.name = some(p.key.string)
while i <= paramCount():
var arg = paramStr(i)
if arg == "--name" and parsingSandboxArgs:
args.name = some(paramStr(i + 1))
i += 2
elif arg == "--profile" and parsingSandboxArgs:
args.profile = some(paramStr(i + 1))
i += 2
elif arg == "--debug" and parsingSandboxArgs:
args.debug = true
i += 1
else:
parsingSandboxArgs = false
command.add(arg)
i += 1
if command.len > 0:
args.cmd = some(command)
if args.name.isSome or args.cmd.isSome or args.profile.isSome:
return some(args) return some(args)
else:
return none(Args)

View File

@@ -1,9 +1,8 @@
import os
import posix import posix
import sequtils import sequtils
type BwrapCall* = object type BwrapCall* = object
args*: seq[string] args: seq[string]
proc addArg*(call: var BwrapCall, args: varargs[string]): var BwrapCall {.discardable.} = proc addArg*(call: var BwrapCall, args: varargs[string]): var BwrapCall {.discardable.} =
for arg in args: for arg in args:
@@ -15,4 +14,4 @@ proc addMount*(call: var BwrapCall, mType: string, path: string): var BwrapCall
call call
proc exec*(call: var BwrapCall) = proc exec*(call: var BwrapCall) =
discard execv("/usr/bin/env", allocCStringArray(@["/usr/bin/env", "bwrap"].concat(call.args))) discard execv("/usr/bin/bwrap", allocCStringArray(@["bwrap"].concat(call.args)))

View File

@@ -3,7 +3,6 @@ import options
import bwrap import bwrap
import utils import utils
import json import json
import os
type Link* = object type Link* = object
src*: string src*: string
@@ -17,14 +16,6 @@ type Config* = object
mountcwd*: Option[bool] mountcwd*: Option[bool]
privileged*: Option[bool] privileged*: Option[bool]
sethostname*: Option[bool] sethostname*: Option[bool]
allowdri*: Option[bool]
dbus*: Option[bool]
dbussee*: Option[seq[string]]
dbustalk*: Option[seq[string]]
dbusown*: Option[seq[string]]
dbuscall*: Option[seq[string]]
dbusbroadcast*: Option[seq[string]]
devmount*: Option[seq[string]]
proc applyConfig*(call: var BwrapCall, config: Config) = proc applyConfig*(call: var BwrapCall, config: Config) =
for mount in config.mount.get(@[]): for mount in config.mount.get(@[]):
@@ -36,14 +27,6 @@ proc applyConfig*(call: var BwrapCall, config: Config) =
for symlink in config.symlinks.get(@[]): for symlink in config.symlinks.get(@[]):
call.addArg("--symlink", symlink.src, symlink.dst) call.addArg("--symlink", symlink.src, symlink.dst)
for device in config.devmount.get(@[]):
call.addArg("--dev-bind", device, device)
if config.mountcwd.get(false):
call
.addMount("--bind", getCurrentDir())
.addArg("--chdir", getCurrentDir())
proc loadConfig*(path: string): Config = proc loadConfig*(path: string): Config =
return readFile(path) return readFile(path)
.parseJson() .parseJson()
@@ -56,20 +39,10 @@ proc extendConfig*(config: var Config): Config {.discardable.} =
var eConf = loadConfig(getProfilePath(config.extends.unsafeGet)) var eConf = loadConfig(getProfilePath(config.extends.unsafeGet))
eConf.extendConfig() eConf.extendConfig()
# todo: replace using macro / templates
config.mount = some(config.mount.get(@[]).concat(eConf.mount.get(@[]))) config.mount = some(config.mount.get(@[]).concat(eConf.mount.get(@[])))
config.romount = some(config.romount.get(@[]).concat(eConf.romount.get(@[]))) config.romount = some(config.romount.get(@[]).concat(eConf.romount.get(@[])))
config.symlinks = some(config.symlinks.get(@[]).concat(eConf.symlinks.get(@[]))) config.symlinks = some(config.symlinks.get(@[]).concat(eConf.symlinks.get(@[])))
config.mountcwd = some(config.mountcwd.get(eConf.mountcwd.get(false))) config.mountcwd = some(config.mountcwd.get(eConf.mountcwd.get(false)))
config.sethostname = some(config.sethostname.get(eConf.sethostname.get(false))) config.sethostname = some(config.sethostname.get(eConf.sethostname.get(false)))
config.allowdri = some(config.allowdri.get(eConf.allowdri.get(false)))
config.devmount = some(config.devmount.get(eConf.devmount.get(@[])))
config.dbus = some(config.dbus.get(eConf.dbus.get(false)))
config.dbussee = some(config.dbussee.get(@[]).concat(eConf.dbussee.get(@[])))
config.dbustalk = some(config.dbustalk.get(@[]).concat(eConf.dbustalk.get(@[])))
config.dbusown = some(config.dbusown.get(@[]).concat(eConf.dbusown.get(@[])))
config.dbuscall = some(config.dbuscall.get(@[]).concat(eConf.dbuscall.get(@[])))
config.dbusbroadcast = some(config.dbusbroadcast.get(@[]).concat(eConf.dbusbroadcast.get(@[])))
return config return config

View File

@@ -1,54 +1,43 @@
import strformat import strformat
import options
import config
import osproc import osproc
import random
import os
type DbusProxy* = object type DbusProxy* = object
process*: Process
socket*: string
args: seq[string] args: seq[string]
proc exec*(proxy: DbusProxy): Process = proc addSee*(proxy: var DbusProxy, name: string): var DbusProxy {.discardable.} =
proxy.args.add(&"--see={name}")
proxy
proc addTalk*(proxy: var DbusProxy, name: string): var DbusProxy {.discardable.} =
proxy.args.add(&"--talk={name}")
proxy
proc addOwn*(proxy: var DbusProxy, name: string): var DbusProxy {.discardable.} =
proxy.args.add(&"--own={name}")
proxy
proc addCall*(proxy: var DbusProxy, name: string): var DbusProxy {.discardable.} =
proxy.args.add(&"--call={name}")
proxy
proc addBroadcast*(proxy: var DbusProxy, name: string): var DbusProxy {.discardable.} =
proxy.args.add(&"--broadcast={name}")
proxy
proc paths*(proxy: var DbusProxy, systembus: string, filterbus: string): var DbusProxy {.discardable.} =
proxy.args.add(&"unix:path={systembus}")
proxy.args.add(filterbus)
proxy
proc log*(proxy: var DbusProxy): var DbusProxy {.discardable.} =
proxy.args.add("--log")
proxy
proc filter*(proxy: var DbusProxy): var DbusProxy {.discardable.} =
proxy.args.add("--filter")
proxy
proc exec*(proxy: DbusProxy): Process {.discardable.} =
# todo: start dbus proxy in bwrap # todo: start dbus proxy in bwrap
# todo: pass arguments as fd # todo: pass arguments as fd
startProcess("xdg-dbus-proxy", args = proxy.args, startProcess("xdg-dbus-proxy", args = proxy.args, options = {poEchoCmd, poParentStreams, poUsePath})
options = {poEchoCmd, poParentStreams, poUsePath})
proc startDBusProxy*(config: Config, hostname: string): DbusProxy =
let busPath = getEnv("DBUS_SESSION_BUS_ADDRESS")
let runtimeDir = getEnv("XDG_RUNTIME_DIR")
if busPath == "" or runtimeDir == "":
raise newException(IOError, "DBUS_SESSION_BUS_ADDRESS and XDG_RUNTIME_DIR are required")
let id = rand(1000)
let filterName = &"dbus-proxy-{hostname}-{id}"
var proxy = DbusProxy()
proxy.socket = &"{runtimeDir}/{filterName}"
proxy.args.add(busPath)
proxy.args.add(proxy.socket)
for name in config.dbussee.get(@[]):
proxy.args.add(&"--see={name}")
for name in config.dbustalk.get(@[]):
proxy.args.add(&"--talk={name}")
for name in config.dbuscall.get(@[]):
proxy.args.add(&"--call={name}")
for name in config.dbusown.get(@[]):
proxy.args.add(&"--own={name}")
for name in config.dbusbroadcast.get(@[]):
proxy.args.add(&"--broadcast={name}")
proxy.args.add("--filter")
proxy.args.add("--log")
proxy.process = proxy.exec()
proxy

View File

@@ -1,13 +1,11 @@
import strutils import os
import sequtils
import options
import config
import utils
import bwrap
import args import args
import json import json
import dbus import dbus
import os import utils
import bwrap
import config
import options
proc sandboxExec*(args: Args) = proc sandboxExec*(args: Args) =
var call = BwrapCall() var call = BwrapCall()
@@ -36,16 +34,28 @@ proc sandboxExec*(args: Args) =
var config = loadConfig(configPath.unsafeGet) var config = loadConfig(configPath.unsafeGet)
config.extendConfig() config.extendConfig()
var proxy = DbusProxy()
proxy
.paths("/run/user/1000/bus", "/run/user/1000/.bus-sandboxed/test2")
.addCall("org.freedesktop.Notifications.*=@/org/freedesktop/Notifications")
.addCall("org.freedesktop.portal.*=*")
.addBroadcast("org.freedesktop.portal.*=@/org/freedesktop/portal/*")
.addOwn("org.mpris.MediaPlayer2.spotify")
.filter()
.log()
.exec()
call call
.addArg("--new-session")
.addArg("--dev", "/dev") .addArg("--dev", "/dev")
# https://github.com/flatpak/flatpak/blob/1bdbb80ac57df437e46fce2cdd63e4ff7704718b/common/flatpak-run.c#L1496
.addMount("--dev-bind", "/dev/dri")
.addMount("--dev-bind", "/dev/nvidiactl")
.addMount("--dev-bind", "/dev/nvidia-modeset")
.addMount("--dev-bind", "/dev/nvidia0")
.addMount("--dev-bind", "/dev/random") .addMount("--dev-bind", "/dev/random")
.addMount("--dev-bind", "/dev/urandom") .addMount("--dev-bind", "/dev/urandom")
.addMount("--ro-bind", "/sys/block") .addArg("--ro-bind", "/run/user/1000/.bus-sandboxed/test2", "/run/user/1000/bus")
.addMount("--ro-bind", "/sys/bus")
.addMount("--ro-bind", "/sys/class")
.addMount("--ro-bind", "/sys/dev")
.addMount("--ro-bind", "/sys/devices")
.addArg("--tmpfs", "/tmp") .addArg("--tmpfs", "/tmp")
.addArg("--tmpfs", "/dev/shm") .addArg("--tmpfs", "/dev/shm")
.addArg("--proc", "/proc") .addArg("--proc", "/proc")
@@ -55,27 +65,13 @@ proc sandboxExec*(args: Args) =
.addArg("--setenv", "BWSANDBOX", "1") .addArg("--setenv", "BWSANDBOX", "1")
.applyConfig(config) .applyConfig(config)
if config.mountcwd.get(false):
call
.addMount("--bind", getCurrentDir())
.addArg("--chdir", getCurrentDir())
if config.sethostname.get(false): if config.sethostname.get(false):
call call
.addArg("--hostname", hostname) .addArg("--hostname", hostname)
if config.dbus.get(false): call.addArg(args.getCmd).exec()
# todo: handle process and cleanup later
let proxy = startDBusProxy(config, hostname)
call.addArg("--ro-bind", proxy.socket,
getEnv("DBUS_SESSION_BUS_ADDRESS").split('=')[1])
# todo: use fd signaling instead of this
sleep(100)
if config.allowdri.get(false):
enableDri(call)
# resolve binary path outside of the sandbox
var cmd = args.getCmd
cmd[0] = findExe(cmd[0])
echo call.args.join(" ")
echo cmd
call.addArg(cmd).exec()

View File

@@ -1,8 +1,5 @@
import strformat
import posix
import bwrap
import args
import os import os
import args
const APP_NAME = "bwsandbox" const APP_NAME = "bwsandbox"
@@ -15,19 +12,9 @@ proc checkRelativePath*(p: string): string =
getHomeDir().joinPath(p) getHomeDir().joinPath(p)
proc getProfilePath*(profile: string): string = proc getProfilePath*(profile: string): string =
let pid = getCurrentProcessId() getConfigDir()
.joinPath(APP_NAME)
for path in [ .joinPath(profile)
getConfigDir().joinPath(APP_NAME),
&"/usr/share/{APP_NAME}",
parentDir(expandSymlink(&"/proc/{pid}/exe")).joinPath("configs")
]:
let file = path.joinPath(profile)
if fileExists(file):
return file
raise newException(IOError, "Profile not found")
proc getProfilePath*(args: Args): string = proc getProfilePath*(args: Args): string =
getProfilePath(args.getProfile()) getProfilePath(args.getProfile())
@@ -36,39 +23,3 @@ proc getSandboxPath*(name: string): string =
getDataDir() getDataDir()
.joinPath(APP_NAME) .joinPath(APP_NAME)
.joinPath(name) .joinPath(name)
proc deviceExists(path: string): bool =
var res: Stat
return stat(path, res) >= 0 and S_ISCHR(res.st_mode)
proc mountDriFolder(call: var BwrapCall, path: string) =
for file in walkPattern(&"{path}/*"):
if dirExists(file):
mountDriFolder(call, file)
elif deviceExists(file):
call.addMount("--dev-bind", file)
#else:
# call.addMount("--ro-bin", file)
# https://github.com/flatpak/flatpak/blob/1bdbb80ac57df437e46fce2cdd63e4ff7704718b/common/flatpak-run.c#L1496
proc enableDri*(call: var BwrapCall) =
const folder = "/dev/dri"
const mounts = [
folder, # general
"/dev/mali", "/dev/mali0", "/dev/umplock", # mali
"/dev/nvidiactl", "/dev/nvidia-modeset", # nvidia
"/dev/nvidia-uvm", "/dev/nvidia-uvm-tools" # nvidia OpenCl/CUDA
]
if dirExists(folder):
mountDriFolder(call, folder)
for mount in mounts:
if deviceExists(mount) or dirExists(mount):
call.addMount("--dev-bind", mount)
for i in 0..20:
let device = &"/dev/nvidia{i}"
if deviceExists(device):
call.addMount("--dev-bind", device)

441
log

File diff suppressed because one or more lines are too long

View File

@@ -1,16 +1,14 @@
import lib/sandbox import lib/sandbox
import lib/args import lib/args
import options import options
import random
proc main(): int = proc main(): int =
let args = parseArgs() let args = parseArgs()
if args.isNone: if args.isNone:
echo "Usage: bwshell --name=sandbox_name --profile=profile <sandbox_cmd>" echo "Usage: bwshell --command=cmd --profile=profile <sandbox_name>"
return 1 return 1
else: else:
randomize()
sandboxExec(args.unsafeGet) sandboxExec(args.unsafeGet)
quit(main()) quit(main())

View File

@@ -1,29 +0,0 @@
#!/run/current-system/sw/bin/bash
if [ $# -ne 1 ]; then
echo "Usage: $0 <target_dir>"
exit 1
fi
check_dir() {
local dir=$1
local file
for application in "$dir/"*; do
file="$(basename "$application")"
sed "s/^Exec=/Exec=bwbox --name '$file' --profile wayland /gi" "$application" > "$target/$file"
done
}
dirs=($(echo "$XDG_DATA_DIRS" | tr ':' '\n'))
dirs+=("$HOME/.local/share")
target="$1"
mkdir -p "$target"
for dir in "${dirs[@]}"; do
if [ -d "$dir/applications" ]; then
check_dir "$dir/applications"
fi
done