From a27a0217d0d5053f55c39c7be3e5986f5b727282 Mon Sep 17 00:00:00 2001
From: Simon McVittie <smcv@collabora.com>
Date: Thu, 24 Jan 2019 20:52:52 +0000
Subject: [PATCH] Separate pressure-vessel-wrap and pressure-vessel-unruntime

Signed-off-by: Simon McVittie <smcv@collabora.com>
---
 README.md                 |  20 +++-
 pressure-vessel-unruntime | 101 ++++++++++++++++++
 pressure-vessel-wrap      | 209 +++++++++++++++-----------------------
 3 files changed, 198 insertions(+), 132 deletions(-)
 create mode 100755 pressure-vessel-unruntime

diff --git a/README.md b/README.md
index 4304ea21a..89fde24b1 100644
--- a/README.md
+++ b/README.md
@@ -115,7 +115,7 @@ Instructions for testing
     - navigate to UserLocalConfigStore/Software/Valve/Steam/Apps/*game ID*
     - below LastPlayed, add:
 
-            "LaunchOptions" "/opt/pressure-vessel/bin/pressure-vessel-wrap -- %command%"
+            "LaunchOptions" "/opt/pressure-vessel/bin/pressure-vessel-unruntime -- %command%"
 
     - restart Steam (on SteamOS use `sudo systemctl restart lightdm`)
 
@@ -132,7 +132,7 @@ Instructions for testing
 
 * To use a runtime instead of the host system, use:
 
-        /opt/pressure-vessel/bin/pressure-vessel-wrap --runtime=$HOME/some-runtime -- %command%
+        /opt/pressure-vessel/bin/pressure-vessel-unruntime --runtime=$HOME/some-runtime -- %command%
 
     The runtime can be either:
 
@@ -151,7 +151,7 @@ Instructions for testing
 
         and then set the launch options to:
 
-            /opt/pressure-vessel/bin/pressure-vessel-wrap --runtime=$HOME/scout_beta-runtime -- %command%
+            /opt/pressure-vessel/bin/pressure-vessel-unruntime --runtime=$HOME/scout_beta-runtime -- %command%
 
     - A sysroot containing `bin/bash`, `lib/ld-linux.so.2`,
       `usr/bin/env`, `usr/share/locale`, `usr/lib/python2.7` and so on,
@@ -200,6 +200,20 @@ Instructions for testing
     The interactive shell's current working directory matches the game's.
     Run `"$@"` in the interactive shell to run the game.
 
+Use `pressure-vessel-unruntime` if you are in a Steam Runtime environment
+(the Steam Runtime's `run.sh` or a Steam game), and `pressure-vessel-wrap`
+if you are not ("Add non-Steam game" in Steam, or a non-Steam-related
+interactive shell).
+
+Steam integration
+-----------------
+
+If a future version of Steam is modified to run certain games using
+pressure-vessel, it should remove the Steam Runtime environment variables
+from the environment with which it runs them, and use `pressure-vessel-wrap`
+directly. `pressure-vessel-unruntime` is just a workaround for this not
+being something that Steam supports yet.
+
 Design constraints
 ------------------
 
diff --git a/pressure-vessel-unruntime b/pressure-vessel-unruntime
new file mode 100755
index 000000000..e2668d719
--- /dev/null
+++ b/pressure-vessel-unruntime
@@ -0,0 +1,101 @@
+#!/bin/bash
+
+# pressure-vessel-unruntime — undo the Steam Runtime's environment
+#
+# Copyright © 2017-2019 Collabora Ltd.
+#
+# SPDX-License-Identifier: MIT
+#
+# Permission is hereby granted, free of charge, to any person obtaining
+# a copy of this software and associated documentation files (the
+# "Software"), to deal in the Software without restriction, including
+# without limitation the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the Software, and to
+# permit persons to whom the Software is furnished to do so, subject to
+# the following conditions:
+#
+# The above copyright notice and this permission notice shall be included
+# in all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+# CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+set -e
+set -o pipefail
+set -u
+shopt -s nullglob
+
+me="$0"
+me="$(readlink -f "$0")"
+here="${me%/*}"
+me="${me##*/}"
+
+default_path="/usr/local/sbin:/usr/sbin:/sbin:/usr/local/bin:/usr/bin:/bin:/usr/local/games:/usr/games"
+
+# Undo any weird environment before we start running external
+# executables. We put it back before running the actual app/game.
+
+declare -a options
+ld_preload=
+options=(--host-fallback)
+
+if [ -n "${LD_LIBRARY_PATH-}" ]; then
+    options+=("--env-if-host=LD_LIBRARY_PATH=$LD_LIBRARY_PATH")
+fi
+
+if [ -n "${LD_PRELOAD-}" ]; then
+    options+=("--env-if-host=LD_PRELOAD=$LD_PRELOAD")
+    ld_preload="$LD_PRELOAD"
+fi
+
+if [ -n "${PATH-}" ]; then
+    options+=("--env-if-host=PATH=$PATH")
+fi
+
+if [ -n "${STEAM_RUNTIME-}" ]; then
+    options+=("--env-if-host=STEAM_RUNTIME=$STEAM_RUNTIME")
+fi
+
+unset LD_LIBRARY_PATH
+unset LD_PRELOAD
+PATH="$default_path"
+unset STEAM_RUNTIME
+
+if [ -n "${SYSTEM_LD_LIBRARY_PATH+set}" ]; then
+    options+=("--env-if-host=SYSTEM_LD_LIBRARY_PATH=$SYSTEM_LD_LIBRARY_PATH")
+    export LD_LIBRARY_PATH="$SYSTEM_LD_LIBRARY_PATH"
+fi
+
+if [ -n "${SYSTEM_LD_PRELOAD+set}" ]; then
+    options+=("--env-if-host=SYSTEM_LD_PRELOAD=$SYSTEM_LD_PRELOAD")
+    export LD_PRELOAD="$SYSTEM_LD_PRELOAD"
+fi
+
+if [ -n "${STEAM_RUNTIME_LIBRARY_PATH+set}" ]; then
+    options+=("--env-if-host=STEAM_RUNTIME_LIBRARY_PATH=$STEAM_RUNTIME_LIBRARY_PATH")
+fi
+
+if [ -n "${SYSTEM_PATH+set}" ]; then
+    options+=("--env-if-host=SYSTEM_PATH=$SYSTEM_PATH")
+    export PATH="$SYSTEM_PATH"
+fi
+
+unset SYSTEM_LD_LIBRARY_PATH
+unset SYSTEM_LD_PRELOAD
+unset SYSTEM_PATH
+
+old_IFS="$IFS"
+IFS=":"
+for word in $ld_preload; do
+    options+=("--host-ld-preload=$word")
+done
+IFS="$old_IFS"
+
+exec "$here/pressure-vessel-wrap" ${options+"${options[@]}"} "$@"
+
+# vim:set sw=4 sts=4 et:
diff --git a/pressure-vessel-wrap b/pressure-vessel-wrap
index 6f2b87a42..80990cd82 100755
--- a/pressure-vessel-wrap
+++ b/pressure-vessel-wrap
@@ -1,8 +1,9 @@
 #!/bin/bash
 
-# pressure-vessel — use containers for Steam
+# pressure-vessel-wrap — run a program in a container that protects $HOME,
+# optionally using a Flatpak-style runtime
 #
-# Copyright © 2017-2018 Collabora Ltd.
+# Copyright © 2017-2019 Collabora Ltd.
 #
 # SPDX-License-Identifier: MIT
 #
@@ -35,46 +36,10 @@ me="$(readlink -f "$0")"
 here="${me%/*}"
 me="${me##*/}"
 
-default_path="/usr/local/sbin:/usr/sbin:/sbin:/usr/local/bin:/usr/bin:/bin:/usr/local/games:/usr/games"
-
-# Undo any weird environment before we start running external
-# executables. We put it back before running the actual app/game.
-
-if { [ -n "${LD_LIBRARY_PATH-}" ] && \
-        [ "${LD_LIBRARY_PATH-}" != "${SYSTEM_LD_LIBRARY_PATH-}" ]; } \
-    || [ -n "${LD_PRELOAD-}" ]; then
-    caller_ld_preload="${LD_PRELOAD:-}"
-    unset LD_PRELOAD
-
-    caller_ld_library_path="${LD_LIBRARY_PATH:-}"
-
-    if [ -n "${SYSTEM_LD_LIBRARY_PATH-}" ]; then
-        export LD_LIBRARY_PATH="$SYSTEM_LD_LIBRARY_PATH"
-    else
-        unset LD_LIBRARY_PATH
-    fi
-
-    caller_path="${PATH:-"${default_path}"}"
-
-    if [ -n "${SYSTEM_PATH-}" ]; then
-        export PATH="$here:$SYSTEM_PATH"
-    else
-        export PATH="$here:$default_path"
-    fi
-
-    if [ -n "$caller_ld_preload" ]; then
-        set -- --ld-preload="$caller_ld_preload" "$@"
-    fi
-
-    if [ -n "$caller_ld_library_path" ]; then
-        set -- --ld-library-path="$caller_ld_library_path" "$@"
-    fi
-
-    if [ -n "$caller_path" ]; then
-        set -- --path="$caller_path" "$@"
-    fi
-
-    exec "$0" "$@"
+if [ -n "${STEAM_RUNTIME+set}" ]; then
+    echo "$me: This program should not be run in the Steam Runtime." >&2
+    echo "$me: Use pressure-vessel-unruntime instead." >&2
+    exit 2
 fi
 
 # If bwrap isn't installed as a standalone binary on the PATH, see
@@ -95,13 +60,14 @@ declare -a bwrap_options
 declare -a multiarch_tuples
 declare -a wrapped_command
 declare -a original_argv
+declare -a ld_preload
+declare -a env_if_host
 original_argv=("$@")
+env_if_host=()
 fake_home=
 host_fallback=
+ld_preload=()
 interactive=
-ld_library_path=
-ld_preload=
-path=
 runtime=
 tmpdir=
 verbose=
@@ -142,16 +108,15 @@ usage () {
     echo "--interactive                 Run an interactive shell instead of"
     echo "                              COMMAND. Executing \"\$@\" in that"
     echo "                              shell will run COMMAND [ARGS]."
-    echo "--ld-library-path=DIR[:DIR]   Set LD_LIBRARY_PATH to DIR[:DIR]"
-    echo "                              when executing COMMAND."
-    echo "--ld-preload=MODULE[:MODULE]  Set LD_PRELOAD to MODULE[:MODULE]"
-    echo "                              when executing COMMAND."
-    echo "--path=DIR[:DIR]              Set PATH to DIR[:DIR] when executing"
-    echo "                              COMMAND."
+    echo "--host-ld-preload=MODULE      Add MODULE from host system to"
+    echo "                              LD_PRELOAD when executing COMMAND."
     echo "--runtime=RUNTIME             Mount the given sysroot or"
     echo "                              merged /usr in the container,"
     echo "                              and augment it with the host"
     echo "                              system's graphics stack."
+    echo "--env-if-host=VAR=VAL         Set VAR=VAL if COMMAND is run with"
+    echo "                              /usr from the host system, but not if"
+    echo "                              it is run with /usr from RUNTIME."
     echo "--verbose                     Be more verbose."
     echo "--xterm                       Same as --interactive, but run an"
     echo "                              xterm in the container."
@@ -161,13 +126,12 @@ usage () {
 }
 
 getopt_temp="help"
+getopt_temp="${getopt_temp},env-if-host:"
 getopt_temp="${getopt_temp},freedesktop-app-id:"
 getopt_temp="${getopt_temp},home:"
 getopt_temp="${getopt_temp},host-fallback"
+getopt_temp="${getopt_temp},host-ld-preload:"
 getopt_temp="${getopt_temp},interactive"
-getopt_temp="${getopt_temp},ld-library-path:"
-getopt_temp="${getopt_temp},ld-preload:"
-getopt_temp="${getopt_temp},path:"
 getopt_temp="${getopt_temp},runtime:"
 getopt_temp="${getopt_temp},steam-app-id:"
 getopt_temp="${getopt_temp},verbose"
@@ -179,6 +143,19 @@ unset getopt_temp
 
 while [ "$#" -gt 0 ]; do
     case "$1" in
+        (--env-if-host)
+            case "$2" in
+                (*=*)
+                    ;;
+                (*)
+                    echo "$me: --env-if-host argument must be VAR=VALUE" >&2
+                    exit 2
+                    ;;
+            esac
+            env_if_host+=("$2")
+            shift 2
+            ;;
+
         (--freedesktop-app-id)
             fake_home="$HOME/.var/app/$2"
             shift 2
@@ -199,24 +176,14 @@ while [ "$#" -gt 0 ]; do
             shift
             ;;
 
-        (--interactive)
-            interactive=yes
-            shift
-            ;;
-
-        (--ld-library-path)
-            ld_library_path="$2"
-            shift 2
-            ;;
-
-        (--ld-preload)
-            ld_preload="$2"
+        (--host-ld-preload)
+            ld_preload+=("host:$2")
             shift 2
             ;;
 
-        (--path)
-            path="$2"
-            shift 2
+        (--interactive)
+            interactive=yes
+            shift
             ;;
 
         (--runtime)
@@ -280,16 +247,17 @@ if [ -n "$verbose" ]; then
     echo "Current working directory:" >&2
     printf '\t%s\n' "Logical: $(pwd -L)" >&2
     printf '\t%s\n' "Physical: $(pwd -P)" >&2
-    echo "Environment:" >&2
-    printf '\t%s\n' "PATH: $path" >&2
-    printf '\t%s\n' "LD_LIBRARY_PATH: $ld_library_path" >&2
-    printf '\t%s\n' "LD_PRELOAD: $ld_preload" >&2
-    echo "Other environment variables:" >&2
+    echo "Environment variables:" >&2
     env | sed -e 's/^/\t/' >&2
 fi
 
 if [ -n "$xterm" ]; then
-    wrapped_command=(xterm -e sh -c 'echo; echo "$1: Starting interactive shell (original command is in \"\$@\")"; echo; shift; exec "$@"' sh "$me" bash -i -s "$@")
+    {
+        # Yes, this weird quoting is really what I mean. Shells within
+        # shells...
+        # shellcheck disable=SC2016
+        wrapped_command=(xterm -e sh -c 'echo; echo "$1: Starting interactive shell (original command is in \"\$@\")"; echo; shift; exec "$@"' sh "$me" bash -i -s "$@")
+    }
 elif [ -n "$interactive" ]; then
     exec </dev/tty
     exec >/dev/tty
@@ -300,6 +268,13 @@ else
     wrapped_command=("$@")
 fi
 
+# Make sure wrapped_command is something we can validly pass to env(1)
+case "${wrapped_command[0]}" in
+    (*=*)
+        wrapped_command=(sh -c 'exec "$@"' sh "${wrapped_command[@]}")
+        ;;
+esac
+
 if [ -z "$BWRAP" ] || ! "$BWRAP" --bind / / true; then
     echo "$me: Cannot find bwrap or it doesn't work" >&2
 
@@ -309,43 +284,16 @@ if [ -z "$BWRAP" ] || ! "$BWRAP" --bind / / true; then
 
     echo "$me: Falling back to executing '${wrapped_command[*]}' directly" >&2
 
-    if [ -n "$ld_preload" ]; then
-        export LD_PRELOAD="$ld_preload"
-    fi
-    if [ -n "$path" ]; then
-        export PATH="$path"
-    else
-        export PATH="$default_path"
-    fi
-    if [ -n "$ld_library_path" ]; then
-        export LD_LIBRARY_PATH="$ld_library_path"
-    else
-        unset LD_LIBRARY_PATH
-    fi
-
-    exec "${wrapped_command[@]}"
+    exec env -- "${env_if_host[@]}" "${wrapped_command[@]}"
 fi
 
 if "$BWRAP" --help 2>&1 | grep -F '[--]' >/dev/null 2>/dev/null; then
     bwrap_end_of_options="--"
 else
-    # bwrap doesn't support the -- syntax
-    bwrap_end_of_options=""
-    case "$1" in
-        (-*)
-            case "$1" in
-                (*=*)
-                    # env would interpret this as an environment variable
-                    echo "$me: Cannot execute command '$1' with bubblewrap < 0.2.2">&2
-                    exit 1
-                    ;;
-                (*)
-                    # Emulate it via env(1)
-                    bwrap_end_of_options="env"
-                    ;;
-            esac
-            ;;
-    esac
+    # bwrap doesn't support the -- syntax, but we ensured that
+    # wrapped_command is not treated as an environment variable
+    # expansion by env, so this is OK
+    bwrap_end_of_options="env"
 fi
 
 cleanup () {
@@ -376,12 +324,6 @@ for t in "${multiarch_tuples[@]}"; do
 done
 
 if [ -n "$runtime" ]; then
-    # The LD_LIBRARY_PATH from the host system is not very relevant
-    # inside the container, so skip it
-    bwrap_options+=(--setenv LD_LIBRARY_PATH "$overrides")
-elif [ -n "$ld_library_path" ]; then
-    bwrap_options+=(--setenv LD_LIBRARY_PATH "$overrides:$ld_library_path")
-else
     bwrap_options+=(--setenv LD_LIBRARY_PATH "$overrides")
 fi
 
@@ -723,10 +665,12 @@ fi
 # We need the LD_PRELOADs from Steam visible at the paths that were
 # used for them, which might be their physical rather than logical
 # locations
-old_ifs="$IFS"
-IFS=:
 adjusted_ld_preload=
-for preload in $ld_preload; do
+for preload in "${ld_preload[@]}"; do
+    # Currently the only preloaded modules we support are ones that come
+    # from the host.
+    preload="${preload#host:}"
+
     if [ -e "$preload" ]; then
         case "$preload" in
             (/usr/*|/lib*)
@@ -745,15 +689,10 @@ for preload in $ld_preload; do
                 adjusted_ld_preload="${adjusted_ld_preload:+"$adjusted_ld_preload:"}$preload"
                 ;;
         esac
+    elif [ -n "$preload" ]; then
+        echo "$me: LD_PRELOAD module '$preload' does not exist" >&2
     fi
 done
-IFS="$old_ifs"
-
-# We need libraries from the Steam Runtime, so make sure that's visible
-# (it should never need to be read/write though)
-if [ -n "${STEAM_RUNTIME:-}" ]; then
-    bwrap_options+=(--ro-bind "$STEAM_RUNTIME" "$STEAM_RUNTIME")
-fi
 
 # Make sure the current working directory (the game we are going to
 # run) is available. Some games write here.
@@ -782,13 +721,8 @@ if [ -n "$adjusted_ld_preload" ]; then
 else
     bwrap_options+=(--unsetenv LD_PRELOAD)
 fi
-if [ -n "$path" ]; then
-    bwrap_options+=(--setenv PATH "$path")
-else
-    bwrap_options+=(--setenv PATH "$default_path")
-fi
 
-if "$BWRAP" --help | grep unshare-uts >/dev/null; then
+if "$BWRAP" --help 2>&1 | grep unshare-uts >/dev/null; then
     # Set a standard hostname for the container to make it easier
     # to see which shell is which
     bwrap_options+=(--hostname pressure-vessel --unshare-uts)
@@ -796,6 +730,23 @@ fi
 
 # Potential future expansion: use --unshare-pid for more isolation
 
+# Put Steam Runtime environment variables back, if /usr is mounted
+# from the host.
+if [ -z "$runtime" ]; then
+    # We need libraries from the Steam Runtime, so make sure that's visible
+    # (it should never need to be read/write though)
+    for env in "${env_if_host[@]}"; do
+        case "$env" in
+            (STEAM_RUNTIME=/*)
+                value="${env#STEAM_RUNTIME=}"
+                bwrap_options+=(--ro-bind "$value" "$value")
+                ;;
+        esac
+    done
+
+    wrapped_command=(env -- "${env_if_host[@]}" "${wrapped_command[@]}")
+fi
+
 # Replace this process with bwrap, which replaces itself with the
 # desired command (unless exec fails)
 if [ -n "$verbose" ]; then
-- 
GitLab