Compare commits

...

14 Commits
dbus ... main

Author SHA1 Message Date
Martin a7763a3b35
Update deps 2024-07-13 19:17:45 +01:00
Martin 5f15f42ffb
Bump dependencies 2023-06-23 15:33:10 +02:00
Martin b48c4d423b
Add runtime dependencies to flake 2023-06-23 15:31:08 +02:00
Martin 8ecfa79ccd
Remove logging 2023-06-23 15:19:26 +02:00
Martin 71eb05c09a
Try to rely less on hardcoded paths 2022-05-12 17:51:55 +02:00
Martin 6ca24383f0
Flakeify 2022-04-07 11:32:09 +02:00
Martin 3a5e5d4870
Extend device support 2021-12-27 16:39:18 +01:00
Martin 77b4fedee2
Format default configs 2021-10-17 10:50:20 +02:00
Martin 93d2163ce9
Fix help output 2021-10-16 13:12:39 +02:00
Martin 1634321bd2
Search multiple locations for profiles 2021-10-16 12:58:11 +02:00
Martin 0bffb6ad35
Dump configs 2021-10-12 10:49:26 +02:00
Martin 9a3e36fcb1
Update argparser to work with launcher mode 2021-08-29 19:40:24 +02:00
Martin 9cad6fc050
Start working on launcher mode 2021-08-14 13:49:26 +02:00
Martin 354ba05faa
add dbus and dri 2021-08-14 13:48:55 +02:00
19 changed files with 772 additions and 52 deletions

4
.gitignore vendored
View File

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

View File

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

13
bwbox.nimble Normal file
View File

@ -0,0 +1,13 @@
# 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"

4
configs/box Normal file
View File

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

View File

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

6
configs/dev Normal file
View File

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

7
configs/gui Normal file
View File

@ -0,0 +1,7 @@
{
"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/*"]
}

5
configs/shell Normal file
View File

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

7
configs/wayland Normal file
View File

@ -0,0 +1,7 @@
{
"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 Normal file
View File

@ -0,0 +1,26 @@
{
"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
}

20
flake.nix Normal file
View File

@ -0,0 +1,20 @@
{
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[string]
cmd*: Option[seq[string]]
profile*: Option[string]
debug*: bool
proc getCmd*(args: Args): string =
return args.cmd.get(getEnv("SHELL", "/bin/bash"))
proc getCmd*(args: Args): seq[string] =
return args.cmd.get(@[getEnv("SHELL", "/bin/sh")])
proc getProfile*(args: Args): string =
if args.profile.isSome:
@ -16,30 +16,34 @@ proc getProfile*(args: Args): string =
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] =
var p = initOptParser()
var args = Args()
var args = Args(debug: false)
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)
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)
else:
return none(Args)

View File

@ -1,8 +1,9 @@
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:
@ -14,4 +15,4 @@ proc addMount*(call: var BwrapCall, mType: string, path: string): var BwrapCall
call
proc exec*(call: var BwrapCall) =
discard execv("/usr/bin/bwrap", allocCStringArray(@["bwrap"].concat(call.args)))
discard execv("/usr/bin/env", allocCStringArray(@["/usr/bin/env", "bwrap"].concat(call.args)))

View File

@ -3,6 +3,7 @@ import options
import bwrap
import utils
import json
import os
type Link* = object
src*: string
@ -16,6 +17,14 @@ 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(@[]):
@ -27,6 +36,14 @@ 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()
@ -39,10 +56,20 @@ 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

54
lib/dbus.nim Normal file
View File

@ -0,0 +1,54 @@
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,16 +1,19 @@
import os
import args
import json
import strutils
import sequtils
import options
import config
import utils
import bwrap
import config
import options
import args
import json
import dbus
import os
proc sandboxExec*(args: Args) =
var call = BwrapCall()
var configPath = none(string)
let hostname = args.name.get(getProfile(argst ))
let hostname = args.name.get(getProfile(args))
if args.name.isSome:
let name = args.name.unsafeGet
@ -18,7 +21,6 @@ proc sandboxExec*(args: Args) =
let sandboxFiles = sandboxPath.joinPath("files")
let userConfig = sandboxPath.joinPath("config.json")
createDir(sandboxFiles)
call.addArg("--bind", sandboxFiles, getHomeDir())
@ -35,10 +37,17 @@ proc sandboxExec*(args: Args) =
config.extendConfig()
call
.addMount("--dev-bind", "/dev/null")
.addArg("--new-session")
.addArg("--dev", "/dev")
.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("--tmpfs", "/tmp")
.addArg("--tmpfs", "/dev/shm")
.addArg("--proc", "/proc")
.addArg("--unshare-all")
.addArg("--share-net")
@ -46,13 +55,27 @@ 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)
call.addArg(args.getCmd).exec()
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()

View File

@ -1,5 +1,8 @@
import os
import strformat
import posix
import bwrap
import args
import os
const APP_NAME = "bwsandbox"
@ -12,9 +15,19 @@ proc checkRelativePath*(p: string): string =
getHomeDir().joinPath(p)
proc getProfilePath*(profile: string): string =
getConfigDir()
.joinPath(APP_NAME)
.joinPath(profile)
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")
proc getProfilePath*(args: Args): string =
getProfilePath(args.getProfile())
@ -23,3 +36,39 @@ 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 Normal file

File diff suppressed because one or more lines are too long

29
scripts/applications.sh Executable file
View File

@ -0,0 +1,29 @@
#!/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