Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add functionality to run commands in a sandbox #72

Closed
wants to merge 10 commits into from
115 changes: 108 additions & 7 deletions util.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import subprocess
import urllib.request
import functools
from pathlib import Path
from socket import socket, AF_INET, SOCK_DGRAM
from typing import Union, Literal, Mapping

Expand Down Expand Up @@ -270,7 +271,7 @@ def protontricks(verb: str) -> bool:
if verb == 'gui':
winetricks_cmd = [winetricks_bin, '--unattended']

# check is verb a custom winetricks verb
# Check is verb a custom winetricks verb
custom_verb = is_custom_verb(verb)
if custom_verb:
winetricks_cmd = [winetricks_bin, '--unattended', custom_verb]
Expand All @@ -282,20 +283,23 @@ def protontricks(verb: str) -> bool:

log.debug('Using winetricks command: ' + str(winetricks_cmd))

# make sure proton waits for winetricks to finish
# Make sure proton waits for winetricks to finish
for idx, arg in enumerate(sys.argv):
if 'waitforexitandrun' not in arg:
sys.argv[idx] = arg.replace('run', 'waitforexitandrun')
log.debug(str(sys.argv))

# Make sure the cache exists
winetricks_cache = os.path.expanduser("~/.cache/winetricks")
if not os.path.exists(winetricks_cache):
os.makedirs(winetricks_cache, exist_ok=True)

# Run winetricks
log.info('Using winetricks verb ' + verb)
subprocess.call([env['WINESERVER'], '-w'], env=env)
with subprocess.Popen(winetricks_cmd, env=env) as process:
process.wait()
_killhanging()
subprocess.run([env['WINESERVER'], '-w'], env=env, check=False)
retc = run_in_sandbox(winetricks_cmd, env)

# Check if the verb failed (eg. access denied)
retc = process.returncode
if retc != 0:
log.warn(f'Winetricks failed running verb "{verb}" with status {retc}.')
return False
Expand Down Expand Up @@ -839,3 +843,100 @@ def set_cpu_topology_limit(core_limit: int, ignore_user_setting: bool = False) -

# Apply the limit
return set_cpu_topology(core_limit, ignore_user_setting)


def run_in_sandbox(cmd: list[str], env: dict[str, str]=None) -> int:
"""Run a command within a sandbox.
The command will run in an temporary environment that is isolated from the
host where only the path to the Proton, WINE prefix, game directory and
winetricks cache directory are read-write and visible to the running
command

When the parent process of the command dies, all of its children will die
with it. A dictionary that contains the user's environment variables is
optional, otherwise the global session environment variables are passed by
default
"""
sandbox_bin = Path('/usr/libexec/steam-runtime-tools-0/srt-bwrap')
env = env or dict(protonmain.g_session.env)
pfx = ""
proton = Path(protondir()).resolve().as_posix()
game = Path(get_game_install_path()).resolve().as_posix()
winetricks_cache = Path.home().joinpath(".cache", "winetricks").resolve().as_posix()
rootfs = []

if os.environ.get("STEAM_COMPAT_DATA_PATH"):
pfx = Path(os.environ.get("STEAM_COMPAT_DATA_PATH")).resolve().as_posix()

if not proton or not pfx:
log.warn("WINEPREFIX or PROTONPATH is not set or empty")
log.warn("Will not execute command")
return 1

if not sandbox_bin.is_file():
log.warn(
f'Failed to find sandboxing tool in {os.environ.get("PRESSURE_VESSEL_RUNTIME")}'
)
log.info('Will execute command on the host')
retc = subprocess.run(
cmd,
check=False,
env=env,
).returncode
_killhanging()
return retc

# Don't execute in a sandbox when using a Flatpak
if os.environ.get('FLATPAK_ID') and sandbox_bin.is_file():
log.info(f'Flatpak environment detected: {os.environ.get("FLATPAK_ID")}')
log.info('Will not execute command in a sandbox')
return subprocess.run(
cmd,
check=False,
env=env,
).returncode

# Mount the root filesystem read-only except for the home directory
for path in Path("/").glob("*"):
if path.name != "home":
posix_path = path.as_posix()
rootfs.extend(["--ro-bind", posix_path, posix_path])

# Unshare all namespaces except the network
opts = [
*rootfs,
'--tmpfs',
'/tmp',
'--dev',
'/dev',
'--proc',
'/proc',
'--die-with-parent',
'--new-session',
'--unshare-all',
'--share-net',
'--disable-userns',
'--unshare-user',
'--bind',
pfx,
pfx,
'--bind',
proton,
proton,
'--bind',
game,
game,
'--bind-try',
winetricks_cache,
winetricks_cache
]

return subprocess.run(
[
sandbox_bin,
*opts,
*cmd,
],
check=False,
env=env,
).returncode