From a3952819e76e6d69657db51ccee83dfb55971b3b Mon Sep 17 00:00:00 2001
From: Etienne Perot <etienne@perot.me>
Date: Sat, 18 May 2024 19:15:57 -0700
Subject: [PATCH] Simplify entrypoint script as per feedback.

No argument parsing is done in the Python script anymore;
instead, a few environment variables do the trick.
---
 Dockerfile                              |   6 +-
 dangerzone/gvisor_wrapper/entrypoint.py | 149 +++++++-----------------
 2 files changed, 41 insertions(+), 114 deletions(-)

diff --git a/Dockerfile b/Dockerfile
index 35574af53..f4c20ffe7 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -75,10 +75,8 @@ COPY conversion /opt/dangerzone/dangerzone/conversion
 # Add the unprivileged user.
 # NOTE: A tmpfs will be mounted over /home/dangerzone directory,
 # so nothing within it from the image will be persisted.
-ARG DANGERZONE_UID=65042
-ARG DANGERZONE_GID=65042
-RUN addgroup -g "$DANGERZONE_GID" dangerzone && \
-    adduser -u "$DANGERZONE_UID" -s /bin/true -G dangerzone -h /home/dangerzone -D dangerzone
+RUN addgroup dangerzone && \
+    adduser -s /bin/true -G dangerzone -h /home/dangerzone -D dangerzone
 
 ###########################################
 # gVisor wrapper image
diff --git a/dangerzone/gvisor_wrapper/entrypoint.py b/dangerzone/gvisor_wrapper/entrypoint.py
index 29d306efb..30ab85212 100755
--- a/dangerzone/gvisor_wrapper/entrypoint.py
+++ b/dangerzone/gvisor_wrapper/entrypoint.py
@@ -1,6 +1,5 @@
 #!/usr/bin/python3
 
-import argparse
 import json
 import os
 import shlex
@@ -10,76 +9,25 @@
 
 # This script wraps the command-line arguments passed to it to run as an
 # unprivileged user in a gVisor sandbox.
+# Its behavior can be modified with the following environment variables:
+#   RUNSC_DEBUG: If set, print debug messages to stderr, and log all gVisor
+#                output to stderr.
+#   RUNSC_FLAGS: If set, pass these flags to the `runsc` invocation.
+# These environment variables are not passed on to the sandboxed process.
 
-# Define flags.
-parser = argparse.ArgumentParser(
-    prog="gvisor_wrapper",
-    description="Run a command in a nested gVisor sandbox",
-    prefix_chars="-",
-    fromfile_prefix_chars=None,
-)
-flags_with_values = []
-parser.add_argument(
-    "--pre_gvisor", action="store_true", help="Run command without gVisor wrapping"
-)
-parser.add_argument(
-    "--gvisor_debug", action="store_true", help="Enable gVisor debug logging"
-)
-parser.add_argument(
-    "--gvisor_strace", action="store_true", help="Enable system call tracing in gVisor"
-)
-flags_with_values.append(
-    parser.add_argument(
-        "--gvisor_flag",
-        action="append",
-        help="Add gVisor flag, may be specified multiple times",
-    )
-)
-parser.add_argument(
-    "command",
-    nargs=argparse.PARSER,
-    help="Command to run in the sandbox (or outside if --pre_gvisor is specified)",
-)
 
-if len(sys.argv) <= 1:  # No arguments: bail.
-    print("No command line specified.", file=sys.stderr)
-    parser.print_help()
-    sys.exit(2)
+def log(message: str, *values: typing.Any) -> None:
+    """Helper function to log messages if RUNSC_DEBUG is set."""
+    if os.environ.get("RUNSC_DEBUG"):
+        print(message.format(*values), file=sys.stderr)
 
-# We can't pass `sys.argv` directly to `parser` here because this may end up
-# processing flags from the wrapped command line that happen to match flags
-# that are defined in `parser`. For example, if the user were to somehow
-# make a file named `--pre_gvisor`, that would be pretty bad.
-# So we scan for the first argument that does not look like it is intended
-# for this entrypoint script, and we only pass this subset to `parser`.
-parser_args = None
-wrapped_command = None
-next_arg_is_value_of_previous_flag = False
-for i, arg in enumerate(sys.argv):
-    if i == 0:
-        continue  # Skip argv[0]
-    if next_arg_is_value_of_previous_flag:
-        next_arg_is_value_of_previous_flag = False
-        continue
-    if arg == "--":
-        parser_args = sys.argv[1:i]
-        wrapped_command = sys.argv[i + 1 :]
-        break
-    if not arg.startswith(parser.prefix_chars):
-        parser_args = sys.argv[1:i]
-        wrapped_command = sys.argv[i:]
-        break
-    if "=" not in arg:
-        arg_name = arg.lstrip(parser.prefix_chars)
-        if any(arg_name == flag.dest for flag in flags_with_values):
-            next_arg_is_value_of_previous_flag = True
-if parser_args is None or wrapped_command is None:  # No command specified.
-    parser_args = sys.argv[1:]
-    wrapped_command = []
+
+wrapped_command = sys.argv[1:]
 if len(wrapped_command) == 0:
+    log("Invoked without a command; will execute 'sh'.")
     wrapped_command = ["sh"]
-parser_args.append("command")  # To satisfy the parser's `command` argument.
-args = parser.parse_args(parser_args)
+else:
+    log("Invoked with command: {}", " ".join(shlex.quote(s) for s in wrapped_command))
 
 # Find the UID/GID of who we should run as within the sandbox.
 sandboxed_uid = int(
@@ -114,7 +62,7 @@
 # This can all be removed and simplified once gvisor.dev/issue/9918 is fixed.
 gvisor_issue_9918_is_fixed = False
 sandbox_capabilities = []
-if not gvisor_issue_9918_is_fixed and not args.pre_gvisor:
+if not gvisor_issue_9918_is_fixed:
     wrapped_command = [
         "su-exec",
         "%d:%d" % (sandboxed_uid, sandboxed_gid),
@@ -201,67 +149,48 @@
         ],
     },
 }
-not_forwarded_env = set(("PATH", "HOME", "SHLVL", "HOSTNAME", "TERM", "PWD"))
+not_forwarded_env = set(
+    (
+        "PATH",
+        "HOME",
+        "SHLVL",
+        "HOSTNAME",
+        "TERM",
+        "PWD",
+        "RUNSC_FLAGS",
+        "RUNSC_DEBUG",
+    )
+)
 for key_val in oci_config["process"]["env"]:
     not_forwarded_env.add(key_val[: key_val.index("=")])
 for key, val in os.environ.items():
     if key in not_forwarded_env:
         continue
     oci_config["process"]["env"].append("%s=%s" % (key, val))
-if args.gvisor_debug:
-    print("Command inside gVisor sandbox:", wrapped_command, file=sys.stderr)
-    print("OCI config:", file=sys.stderr)
+if os.environ.get("RUNSC_DEBUG"):
+    log("Command inside gVisor sandbox: {}", wrapped_command)
+    log("OCI config:")
     json.dump(oci_config, sys.stderr, indent=2, sort_keys=True)
     # json.dump doesn't print a trailing newline, so print one here:
-    print("", file=sys.stderr)
+    log("")
 with open("/dangerzone-image/config.json", "w") as oci_config_out:
     json.dump(oci_config, oci_config_out, indent=2, sort_keys=True)
 
 # Run gVisor.
-runsc_binary = "/usr/bin/runsc"
-runsc_argv = [os.path.basename(runsc_binary), "--rootless=true", "--network=none"]
-if args.gvisor_debug:
+runsc_argv = ["/usr/bin/runsc", "--rootless=true", "--network=none"]
+if os.environ.get("RUNSC_DEBUG"):
     runsc_argv += ["--debug=true", "--alsologtostderr=true"]
-if args.gvisor_strace:
-    runsc_argv += ["--strace=true"]
-if args.gvisor_flag is not None:
-    for gvisor_flag in args.gvisor_flag:
-        if gvisor_flag:
-            runsc_argv += [gvisor_flag]
+if os.environ.get("RUNSC_FLAGS"):
+    runsc_argv += [x for x in shlex.split(os.environ.get("RUNSC_FLAGS", "")) if x]
 runsc_argv += ["run", "--bundle=/dangerzone-image", "dangerzone"]
-
-# Check for `--pre_gvisor` which can be used to run commands without wrapping.
-if args.pre_gvisor:
-    print(
-        "Would be running the following command if --pre_gvisor had not been specified:",
-        " ".join(shlex.quote(s) for s in runsc_argv),
-        file=sys.stderr,
-    )
-    print(
-        "Executing this command instead:",
-        " ".join(shlex.quote(s) for s in wrapped_command),
-        file=sys.stderr,
-    )
-    try:
-        os.execvp(wrapped_command[0], wrapped_command)
-    except Exception as e:
-        raise e.__class__("Process %s failed: %s" % (wrapped_command, e))
-    else:
-        assert False, "This code should never be reachable"
-
-if args.gvisor_debug:
-    print(
-        "Running gVisor with command line:",
-        " ".join(shlex.quote(s) for s in runsc_argv),
-        file=sys.stderr,
-    )
+log(
+    "Running gVisor with command line: {}", " ".join(shlex.quote(s) for s in runsc_argv)
+)
 runsc_process = subprocess.run(
     runsc_argv,
-    executable=runsc_binary,
     check=False,
 )
-if args.gvisor_debug:
-    print("gVisor quit with exit code:", runsc_process.returncode, file=sys.stderr)
+log("gVisor quit with exit code: {}", runsc_process.returncode)
 
 # We're done.
 sys.exit(runsc_process.returncode)