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)