From 0615e9bf4e34deadeb03eaf8256d95879d855fd1 Mon Sep 17 00:00:00 2001 From: Etienne Perot Date: Sun, 8 Oct 2023 16:22:07 -0700 Subject: [PATCH] On Linux, try to use Docker with gVisor if installed, else use Podman. gVisor is an open-source OCI-compliant container runtime. It is a userspace reimplementation of the Linux kernel in a memory-safe language. It works by creating a sandboxed environment in which regular Linux applications run, but their system calls are intercepted by gVisor. gVisor then redirects these system calls and reinterprets them in its own kernel. This means the host Linux kernel is isolated from the sandboxed application, thereby providing protection against Linux container escape attacks. It also uses `seccomp-bpf` to provide a secondary layer of defense against container escapes. Even if its userspace kernel gets compromised, attackers would have to additionally have a Linux container escape vector, and that exploit would have to fit within the restricted `seccomp-bpf` rules that gVisor adds on itself. --- INSTALL.md | 28 ++++++++-- dangerzone/gui/main_window.py | 6 ++- dangerzone/isolation_provider/container.py | 61 ++++++++++++++++------ tests/test_ocr.py | 7 ++- 4 files changed, 78 insertions(+), 24 deletions(-) diff --git a/INSTALL.md b/INSTALL.md index 6dadf6af2..441335c01 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -5,8 +5,10 @@ See instructions in [README.md](README.md#macos). See instructions in [README.md](README.md#windows). ## Linux -On Linux, Dangerzone uses [Podman](https://podman.io/) instead of Docker Desktop for creating -an isolated environment. It will be installed automatically when installing Dangerzone. +On Linux, Dangerzone uses either [Docker](https://www.docker.com/) CLI (if +[gVisor](https://gvisor.dev) is installed as a runtime), or +[Podman](https://podman.io/) for creating an isolated environment. It will be +installed automatically when installing Dangerzone. Dangerzone is available for: - Ubuntu 23.04 (lunar) @@ -26,9 +28,25 @@ Dangerzone is available for: :memo: Expand this section if you are on Ubuntu 20.04 (Focal).
- Dangerzone requires [Podman](https://podman.io/), which is not available - through the official Ubuntu Focal repos. To proceed with the Dangerzone - installation, you need to add an extra OpenSUSE repo that provides Podman to + Dangerzone requires either: + + * [Docker](https://www.docker.com/) with [gVisor](https://gvisor.dev), or + * [Podman](https://podman.io/) + + Neither of these are available through the official Ubuntu Focal repos. + + For Docker with gVisor, follow the + [Docker installation instructions](https://docs.docker.com/engine/install/ubuntu/), + followed by the + [gVisor installation instructions](https://gvisor.dev/docs/user_guide/install/). + If all goes well, you should be able to run the following and observe the gVisor + kernel startup messages: + + ```bash + docker run --runtime=runsc --rm alpine:latest dmesg + ``` + + For Podman, you need to add an extra OpenSUSE repo that provides Podman to Ubuntu Focal users. You can follow the instructions below, which have been copied from the [official Podman blog](https://podman.io/new/2021/06/16/new.html): diff --git a/dangerzone/gui/main_window.py b/dangerzone/gui/main_window.py index 408f10969..82f374fad 100644 --- a/dangerzone/gui/main_window.py +++ b/dangerzone/gui/main_window.py @@ -400,7 +400,9 @@ def check_state(self) -> None: if isinstance( # Sanity check self.dangerzone.isolation_provider, Container ): - container_runtime = self.dangerzone.isolation_provider.get_runtime() + container_runtime_image_args = ( + self.dangerzone.isolation_provider.get_runtime_args("image") + ) except NoContainerTechException as e: log.error(str(e)) state = "not_installed" @@ -408,7 +410,7 @@ def check_state(self) -> None: else: # Can we run `docker image ls` without an error with subprocess.Popen( - [container_runtime, "image", "ls"], + container_runtime_image_args + ["ls"], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, startupinfo=get_subprocess_startupinfo(), diff --git a/dangerzone/isolation_provider/container.py b/dangerzone/isolation_provider/container.py index 61d641b78..441e0cd80 100644 --- a/dangerzone/isolation_provider/container.py +++ b/dangerzone/isolation_provider/container.py @@ -50,22 +50,54 @@ def __init__(self, enable_timeouts: bool) -> None: self.enable_timeouts = 1 if enable_timeouts else 0 super().__init__() + @staticmethod + def get_gvisor_docker_runtime() -> Optional[str]: + if platform.system() != "Linux": + return None + if shutil.which("docker") is None: + return None + try: + return ( + subprocess.check_output( + [ + "docker", + "info", + "--format", + '{{range $i, $v := .Runtimes}}{{if eq $i "runsc"}}runsc{{end}}{{end}}', + ], + text=True, + startupinfo=get_subprocess_startupinfo(), + ) + or None + ) + except Exception as e: + return None + @staticmethod def get_runtime_name() -> str: if platform.system() == "Linux": - runtime_name = "podman" + if Container.get_gvisor_docker_runtime() is not None: + return "docker" + return "podman" else: # Windows, Darwin, and unknown use docker for now, dangerzone-vm eventually - runtime_name = "docker" - return runtime_name + return "docker" @staticmethod - def get_runtime() -> str: + def get_runtime_args(subcommand: str) -> List[str]: container_tech = Container.get_runtime_name() runtime = shutil.which(container_tech) if runtime is None: raise NoContainerTechException(container_tech) - return runtime + runtime_args: List[str] = [] + subcommand_args: List[str] = [] + if ( + container_tech == "docker" + and subcommand in ("run", "create") + and Container.get_gvisor_docker_runtime() is not None + ): + subcommand_args.extend(("--runtime", "runsc")) + return [runtime] + runtime_args + [subcommand] + subcommand_args @staticmethod def install() -> bool: @@ -79,7 +111,7 @@ def install() -> bool: log.info("Installing Dangerzone container image...") p = subprocess.Popen( - [Container.get_runtime(), "load"], + Container.get_runtime_args("load"), stdin=subprocess.PIPE, startupinfo=get_subprocess_startupinfo(), ) @@ -115,9 +147,8 @@ def is_container_installed() -> bool: # See if this image is already installed installed = False found_image_id = subprocess.check_output( - [ - Container.get_runtime(), - "image", + Container.get_runtime_args("image") + + [ "list", "--format", "{{.ID}}", @@ -137,7 +168,7 @@ def is_container_installed() -> bool: try: subprocess.check_output( - [Container.get_runtime(), "rmi", "--force", found_image_id], + Container.get_runtime_args("rmi") + ["--force", found_image_id], startupinfo=get_subprocess_startupinfo(), ) except: @@ -207,7 +238,7 @@ def exec_container( command: List[str], extra_args: List[str] = [], ) -> int: - container_runtime = self.get_runtime() + container_runtime_args = self.get_runtime_args("run") if self.get_runtime_name() == "podman": security_args = ["--security-opt", "no-new-privileges"] @@ -221,8 +252,8 @@ def exec_container( prevent_leakage_args = ["--rm"] - args = ( - ["run", "--network", "none"] + run_args = ( + ["--network", "none"] + user_args + security_args + prevent_leakage_args @@ -231,7 +262,7 @@ def exec_container( + command ) - args = [container_runtime] + args + args = container_runtime_args + run_args return self.exec(document, args) def _convert( @@ -374,7 +405,7 @@ def get_max_parallel_conversions(self) -> int: # For Windows and MacOS containers run in VM # So we obtain the CPU count for the VM n_cpu_str = subprocess.check_output( - [self.get_runtime(), "info", "--format", "{{.NCPU}}"], + self.get_runtime_args("info") + ["--format", "{{.NCPU}}"], text=True, startupinfo=get_subprocess_startupinfo(), ) diff --git a/tests/test_ocr.py b/tests/test_ocr.py index 1e9f3a0bf..1f1571637 100644 --- a/tests/test_ocr.py +++ b/tests/test_ocr.py @@ -15,8 +15,11 @@ def test_ocr_ommisions() -> None: # Create the command that will list all the installed languages in the container # image. - runtime = Container.get_runtime() - command = [runtime, "run", Container.CONTAINER_NAME, "tesseract", "--list-langs"] + command = Container.get_runtime_args("run") + [ + Container.CONTAINER_NAME, + "tesseract", + "--list-langs", + ] # Run the command, strip any extra whitespace, and remove the following first line # from the result: