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
bwbox
result
scripts/applications
main

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": [],
"romount": ["/etc", "/var", "/usr", "/opt"],
"romount": ["/etc", "/var", "/usr", "/opt", ".oh-my-zsh", ".zsh", ".zshrc"],
"symlinks": [
{"src": "usr/lib", "dst": "/lib"},
{"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 os
type Args* = object
name*: Option[string]
cmd*: Option[seq[string]]
cmd*: Option[string]
profile*: Option[string]
debug*: bool
proc getCmd*(args: Args): seq[string] =
return args.cmd.get(@[getEnv("SHELL", "/bin/sh")])
proc getCmd*(args: Args): string =
return args.cmd.get(getEnv("SHELL", "/bin/bash"))
proc getProfile*(args: Args): string =
if args.profile.isSome:
@@ -16,34 +16,30 @@ proc getProfile*(args: Args): string =
return "default"
proc parseArgs*(): Option[Args] =
var args = Args(debug: false)
var command = newSeq[string]()
var parsingSandboxArgs = true
var i = 1
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)
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 none(Args)
return false
return true
proc parseArgs*(): Option[Args] =
var p = initOptParser()
var args = Args()
while true:
p.next()
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)
return some(args)

View File

@@ -1,9 +1,8 @@
import os
import posix
import sequtils
type BwrapCall* = object
args*: seq[string]
args: seq[string]
proc addArg*(call: var BwrapCall, args: varargs[string]): var BwrapCall {.discardable.} =
for arg in args:
@@ -15,4 +14,4 @@ proc addMount*(call: var BwrapCall, mType: string, path: string): var BwrapCall
call
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 utils
import json
import os
type Link* = object
src*: string
@@ -17,14 +16,6 @@ type Config* = object
mountcwd*: Option[bool]
privileged*: 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) =
for mount in config.mount.get(@[]):
@@ -36,14 +27,6 @@ proc applyConfig*(call: var BwrapCall, config: Config) =
for symlink in config.symlinks.get(@[]):
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 =
return readFile(path)
.parseJson()
@@ -56,20 +39,10 @@ proc extendConfig*(config: var Config): Config {.discardable.} =
var eConf = loadConfig(getProfilePath(config.extends.unsafeGet))
eConf.extendConfig()
# todo: replace using macro / templates
config.mount = some(config.mount.get(@[]).concat(eConf.mount.get(@[])))
config.romount = some(config.romount.get(@[]).concat(eConf.romount.get(@[])))
config.symlinks = some(config.symlinks.get(@[]).concat(eConf.symlinks.get(@[])))
config.mountcwd = some(config.mountcwd.get(eConf.mountcwd.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

View File

@@ -1,54 +1,43 @@
import strformat
import options
import config
import osproc
import random
import os
type DbusProxy* = object
process*: Process
socket*: 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: pass arguments as fd
startProcess("xdg-dbus-proxy", args = proxy.args,
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
startProcess("xdg-dbus-proxy", args = proxy.args, options = {poEchoCmd, poParentStreams, poUsePath})

View File

@@ -1,13 +1,11 @@
import strutils
import sequtils
import options
import config
import utils
import bwrap
import os
import args
import json
import dbus
import os
import utils
import bwrap
import config
import options
proc sandboxExec*(args: Args) =
var call = BwrapCall()
@@ -36,16 +34,28 @@ proc sandboxExec*(args: Args) =
var config = loadConfig(configPath.unsafeGet)
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
.addArg("--new-session")
.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/urandom")
.addMount("--ro-bind", "/sys/block")
.addMount("--ro-bind", "/sys/bus")
.addMount("--ro-bind", "/sys/class")
.addMount("--ro-bind", "/sys/dev")
.addMount("--ro-bind", "/sys/devices")
.addArg("--ro-bind", "/run/user/1000/.bus-sandboxed/test2", "/run/user/1000/bus")
.addArg("--tmpfs", "/tmp")
.addArg("--tmpfs", "/dev/shm")
.addArg("--proc", "/proc")
@@ -55,27 +65,13 @@ proc sandboxExec*(args: Args) =
.addArg("--setenv", "BWSANDBOX", "1")
.applyConfig(config)
if config.mountcwd.get(false):
call
.addMount("--bind", getCurrentDir())
.addArg("--chdir", getCurrentDir())
if config.sethostname.get(false):
call
.addArg("--hostname", hostname)
if config.dbus.get(false):
# 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()
call.addArg(args.getCmd).exec()

View File

@@ -1,8 +1,5 @@
import strformat
import posix
import bwrap
import args
import os
import args
const APP_NAME = "bwsandbox"
@@ -15,19 +12,9 @@ proc checkRelativePath*(p: string): string =
getHomeDir().joinPath(p)
proc getProfilePath*(profile: string): string =
let pid = getCurrentProcessId()
for path in [
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")
getConfigDir()
.joinPath(APP_NAME)
.joinPath(profile)
proc getProfilePath*(args: Args): string =
getProfilePath(args.getProfile())
@@ -36,39 +23,3 @@ proc getSandboxPath*(name: string): string =
getDataDir()
.joinPath(APP_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/args
import options
import random
proc main(): int =
let args = parseArgs()
if args.isNone:
echo "Usage: bwshell --name=sandbox_name --profile=profile <sandbox_cmd>"
echo "Usage: bwshell --command=cmd --profile=profile <sandbox_name>"
return 1
else:
randomize()
sandboxExec(args.unsafeGet)
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