Compare commits

1 Commits

Author SHA1 Message Date
67ea99cd38 Start working on launcher mode 2021-08-14 13:15:07 +02:00
19 changed files with 53 additions and 745 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,3 +1,4 @@
import parseopt
import options import options
import os import os
@@ -5,10 +6,9 @@ type Args* = object
name*: Option[string] name*: Option[string]
cmd*: Option[seq[string]] cmd*: Option[seq[string]]
profile*: Option[string] profile*: Option[string]
debug*: bool
proc getCmd*(args: Args): seq[string] = proc getCmd*(args: Args): seq[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,34 @@ proc getProfile*(args: Args): string =
return "default" return "default"
proc parseArgs*(): Option[Args] = proc parseOpt(args: var Args, key: string, value: string): bool =
var args = Args(debug: false) case key
of "name", "n":
var command = newSeq[string]() args.name = some(value)
var parsingSandboxArgs = true of "profile", "p":
var i = 1 args.profile = some(value)
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: else:
parsingSandboxArgs = false return false
command.add(arg)
i += 1 return true
proc parseArgs*(): Option[Args] =
var p = initOptParser()
var args = Args()
var command = newSeq[string]()
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:
command.add(p.key.string)
if command.len > 0: if command.len > 0:
args.cmd = some(command) 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 +0,0 @@
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 =
# 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

View File

@@ -1,13 +1,10 @@
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 utils
import os import bwrap
import config
import options
proc sandboxExec*(args: Args) = proc sandboxExec*(args: Args) =
var call = BwrapCall() var call = BwrapCall()
@@ -37,17 +34,10 @@ proc sandboxExec*(args: Args) =
config.extendConfig() config.extendConfig()
call call
.addArg("--new-session") .addMount("--dev-bind", "/dev/null")
.addArg("--dev", "/dev")
.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")
.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("--proc", "/proc") .addArg("--proc", "/proc")
.addArg("--unshare-all") .addArg("--unshare-all")
.addArg("--share-net") .addArg("--share-net")
@@ -55,27 +45,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,15 @@
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()
echo args
if args.isNone: if args.isNone:
echo "Usage: bwshell --name=sandbox_name --profile=profile <sandbox_cmd>" echo "Usage: bwshell --name=sandbox_name --profile=profile <sandbox_cmd>"
return 1 return 1
else: else:
randomize()
sandboxExec(args.unsafeGet) sandboxExec(args.unsafeGet)
quit(main()) quit(main())

View File

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