TL;DR
Having a sandbox doesn’t matter much if your fancy LSP-enabled editor runs outside of it. To solve this issue, I created a script that can inject a binary-patched portable editor into any container via --mount type=image. It’s less secure than a VM, especially considering the many recent Linux LPEs, but it’s very convenient.
Introduction
Lately, I’ve been concerned about editors.
With the increasing frequency of self-propagating worms across package management ecosystems, I believe the chance of running an infected package is high enough that it needs to be accounted for when setting up a development environment. What’s more, my personal machine is arguably more valuable than a typical corporate desktop; it contains my banking information, health records, personal correspondence, etc. If I am to do development work on my local machine, sandboxing is an absolute must.
One common solution to this problem, which I use here, is to develop in a container.
Plain Containers for Development
While some may object to the use of a container as a security boundary (particularly with the shocking number of Linux LPE vulnerabilities being discovered by LLMs), the container ecosystem is amazing to work with. In particular, the Dockerfile/Containerfile specification makes it easy to package software in a way that eliminates “works on my machine” issues for most workloads. Most virtual machine setups I’ve interacted with, on the other hand, have been either cumbersome or feature-incomplete. For example, consider a situation where I need to run a GPU-accelerated workload or connect to a microcontroller over USB. With a crun/runc container, this is as simple as passing the relevant devices to the container and possibly adjusting some subgid mappings on the host. Running the container itself can even be done without root, thanks to rootless Podman/Docker.
In the VM world, support for anything other than pure compute and networking is hit-or-miss. A microVM launched through Firecracker or Kata doesn’t support any of the devices I care about (non-exclusive GPU access, for example), while a full QEMU VM (as run through GNOME Boxes or virt-manager) has a long start-up time and lacks integration with container tooling. Even dedicated “secure” container runtimes like gVisor don’t support passing arbitrary devices through (GPU compute only works with CUDA, and only on specific driver versions) and come with significant performance tradeoffs. I’m also not a fan of Dev Containers, as the devcontainer.json file provided by a repository can be configured in a malicious manner (mounting arbitrary files, adding --privileged, etc.). While I can tolerate some level of jank to ensure security, these tools inhibit my development workflow enough to make them unviable for me.
Editor Vulnerabilities
However, even using a perfect sandbox when building/running code wouldn’t keep you safe if the editor you use to write the code ends up running it automatically. This is quite common when using language servers that must interpret code in order to check it for correctness. For example, rust-analyzer will run arbitrary code in the form of both build scripts and procedural macros. Some editors (like VSCode and Zed) have realized that this is an issue and will give the user the option of using “Restricted Mode,” which disables language servers. However, I consider language servers an essential part of modern development (I frequently hover over function names to preview relevant documentation, for example), so disabling them is not an acceptable option for me. Furthermore, at least on Zed, Restricted Mode doesn’t actually keep you very safe (see issues #45856, #48300, and the embarrassingly longstanding #12589). Remoting into a container isn’t a solution either, as connecting to untrusted servers can result in local code execution in VSCode (as far as I’m aware, the threat model for SSH development in Zed has not been publicly documented). VSCode also has the same issue with Dev Containers.
Launching an Editor Inside of a Container
The solution to these issues is, of course, to run your editor within the container. Terminal editors can easily be run in containers using the standard terminal-focused container commands you’re probably already familiar with (podman run -it ...). Running GUI editors is much more involved: you must mount your windowing (i.e. Wayland or X) socket and graphics device inside the container (this increases the attack surface, but can’t easily be avoided), add the render group via --group-add, adjust container GID mappings, and let the host user that runs the container subgid the render group via /etc/subgid.
Regardless of which type of editor you prefer, you must first ensure that your preferred editor is present within the container image. If you’re using a random image from Docker Hub or a Git repository, this likely isn’t the case. I’ve worked around this issue by building a container image that contains my preferred editor (Zed) and its relevant dependencies, with all binaries in the image patched to use the image’s own libraries (via RPATH) and ld-linux. This not only fixes issues with missing libraries and libc compatibility but also lets me use helper binaries from the editor image without explicitly setting LD_LIBRARY_PATH. I can then use an image mount to mount the editor into any container I want and change the entrypoint of the container to point to the editor. Because I prefer to run Zed with an invalid proxy setting to mitigate the aforementioned #12589, I also mount a separate extensions folder, whose contents are built by a local builder container. Using this method, I can have an enjoyable development experience given any container image. Here’s a simplified example of the command my script runs to start node:latest using this method:
podman run \
--rm \
-it \
--mount type=image,source=zed-portable,target=/usr/zed \
-e XDG_RUNTIME_DIR=/tmp/runtime-mounted \
-e WAYLAND_DISPLAY=wayland-0 \
-v $XDG_RUNTIME_DIR/$WAYLAND_DISPLAY:/tmp/runtime-mounted/wayland-0 \
--device=/dev/dri/renderD128 \
--gidmap "+g10990:@990" \
--group-entry "render:x:10990:" \
--group-add 10990 \
--entrypoint="/usr/zed/usr/bin/bash" \
node:latest \
-i \
-c \
'set -a
ZED_ALLOW_ROOT=true
XKB_CONFIG_ROOT=/usr/zed/usr/share/X11/xkb
VK_DRIVER_FILES=/usr/zed/usr/share/vulkan/icd.d
LIBGL_DRIVERS_PATH=/usr/zed/usr/lib/x86_64-linux-gnu/dri
__EGL_VENDOR_LIBRARY_DIRS=/usr/zed/usr/share/glvnd/egl_vendor.d
EGL_DRIVERS_PATH=/usr/zed/usr/lib/x86_64-linux-gnu/dri
FONTCONFIG_FILE=/usr/zed/etc/fonts/fonts.conf
ZED_STATELESS=1
XCOMPOSEFILE="/usr/zed/usr/share/X11/locale/en_US.UTF-8/Compose"
/usr/zed/root/.local/zed-preview.app/bin/zed \
--foreground'Further Work
This system still isn’t completely secure, however. Some Wayland compositors support protocols that could be considered invasive, such as screen sharing. Nearly all of them will allow a currently focused window to read the clipboard contents, which is an issue if you use a password manager. I use wl-mitm to mitigate these risks, which is a topic for a future blog post. Other security angles to be considered include the aforementioned Linux LPEs (keep your system updated!), local network access, resource exhaustion, and side-channel attacks. For additional isolation, this solution could be run inside of a virtual machine or as a different user. More work is needed to securely handle credentials; I use a credential injection proxy for Git, but I don’t yet feel comfortable sharing my setup until I’ve thoroughly battle-tested it.
I’m also quite interested in the krun project of crun, which could be used to spin up a graphics-accelerated microVM without the DRM attack surface. Libkrun even supports attaching to an existing Wayland socket for seamless windowing integration. Unfortunately, it seems that many of the dependencies necessary for this to work (such as Rutabaga) are not yet shipped in Ubuntu. I’ll definitely keep an eye on this going forward.
You can find my scripts/Dockerfiles for this setup here: https://github.com/BenCantCode/containeri-zed
Please email me if you have any feedback on this system or are using it yourself; I’d love to hear what you think.
Ben Garvin