37 comments

[ 0.23 ms ] story [ 24.1 ms ] thread
> I wanted specifically to find a minimal set of restrictions to run untrusted code.

I don't think we should consider containers to be a security boundary. Even full VMs can be escaped, and have been, many times.

The fact that this is possible in the first place makes me think we need a much better approach.

I definitely wouldn't trust standard Linux style containers that expose a shared Linux kernel at the moment, there's been far too many LPE and container breakout vulnerabilities this year. It's possible that in future if the kernel gets a lot more hardened, that could change but things like Firecracker are a better bet from a security standpoint.
Podman supports using KVM backed virtualization for containers via libkrun: `podman run --runtime=krun` . Still not the end-all-be-all security boundary, but better I think.
Presumably that brings in issues with resource passthrough like files or GPUs etc? It’s one of the reasons I like LXCs.
Depends on the threat model. Security is not black-and-white.

Containers protect against "I don't trust this curlpipe to not crap all over my dotfiles," rather than, "there might be a sandbox escape attack in this random file I downloaded."

If a VM is not sufficient for your threat model, I'm curious what is?

I think until something hardware-based like CHERI becomes widely deployed (which seems extremely unlikely in the near to mid term given), we're going to keep seeing VM escape CVEs pop up indefinitely.
Because we've never encountered hardware bugs…
Critical hardware bugs occur an order of magnitude less frequently than critical hypervisor/kernel bugs, which is why they always make the news. In general, they're also more difficult to exploit.
But when they get discovered the only solution is to redesign the thing and throw away all of the hardware, with no mitigation strategy in between.

Some might prefer issues that can be fixed instantly and cheap to issues that will require years and millions to be fixed.

You seem to be arguing on the basis that people are throwing away the software security layers, when the discussion so far has been about hardening the software security layers even more with secure hardware.

This is the nirvana fallacy in action. Because you cannot imagine a world with perfect hardware, you think hardware that catches 99.9% of security bugs is worthless.

You seem to be discarding budget problems. Yes you can do a lot more things if you have unlimited budget, but that is rarely the case. So most solutions won't be implemented if they aren't also relatively inexpensive.
They are a security boundary, but like everything else, not perfect.
We don't have any "security boundaries" by this definition, just "security make-it-harder"s.
The principle of defense in depth is built around the idea that with enough time, any system can likely be compromised, but the chance of compromising the system before being detected in your attempts to do so is lower the more safeguards you put into place
I use per project bubblewrap scripts for my coding agents.
Container escape is already vanishingly small amount of attacks.

Like, if app in wild gets hacked, you kinda already are screwed, even if the hack is contained to the box (whether VM or container) app runs in, you still get whatever app keys app used, and you still get whatever visibility to internal network the app had. https://xkcd.com/1200/ basically.

If your app server gets hacked, all user data leaks anyway. If you divide everything to microservices so they see minimum required amount of data, attacker can still see everything that goes thru it

That a security measure or boundary can be broken doesn't mean it is not a security measure. In fact that breakage occurences are so rare that they constitute news means that they are somewhat effective at that.
Coincidentally I mis-prompted claude code the other day while working on a toy project and failed to specify the project should be built on top of docker and not "like docker".

It went on to waste all my tokens creating a specialized docker clone. Cool I guess.

Last night I was looking for how to run Graphviz on untrusted input in a secure way, because recent versions of Graphviz give untrusted input to a whole insane rat's nest of code: Harfbuzz, Pango, libfribidi, libthai, libgraphite2, and its own internal format parser, each of which has a rap sheet of CVEs that makes Charlie Manson look like a petty shoplifter. And apparently Pango is even multithreaded, so we can expect nondeterminism. (Most of this doesn't show up in a simple ldd check; Graphviz sneakily waits until runtime to dlopen graphviz/libgvplugin_pango.so.6!) So, naturally, I wanted to sandbox it so that the worst thing a malicious attacker could do would be to make it draw Dickbutt or something. What I ended up with was less than 500 lines of code using Claude's suggestion of Bubblewrap http://canonical.org/~kragen/sw/dev3/wrapdot:

    #!/bin/sh
    # Confine dot in bubblewrap, taking input from stdin and writing PNG
    # output to stdout.

    # 64 megs seems to be enough, 21 megs isn’t.
    address_space=64001000

    # With zero --fsize, we can’t write the output file on stdout if it's
    # redirected to a file, but you can pipe it to `cat`.
    file_size=0

    cpu_seconds=5

    # Apparently Pango or fontconfig is multithreaded now‽
    # (process:2): GLib-ERROR **: 00:37:18.600: creating thread '[pango] FcInit': Error creating thread: Resource temporarily unavailable
    processes=4

    # We’re using --unshare-user, etc., explicitly, because --unshare-all
    # uses the wimpy --unshare-user-try and --unshare-cgroup-try options.
    # --remount-ro / prevents malicious code from filling the root
    # filesystem with empty files.

    exec bwrap \
          --ro-bind /bin /bin \
          --ro-bind /lib /lib \
          --ro-bind /lib64 /lib64 \
          --ro-bind /sbin /sbin \
          --ro-bind /usr/lib /usr/lib \
          --ro-bind /usr/share/fonts /usr/share/fonts \
          --ro-bind /var/cache/fontconfig /var/cache/fontconfig \
          --ro-bind /etc/fonts /etc/fonts \
          --remount-ro / \
          --unshare-user --unshare-ipc --unshare-pid --unshare-net --unshare-uts \
          --unshare-cgroup --die-with-parent --new-session --cap-drop ALL \
          --clearenv --setenv PATH /bin \
          prlimit --as="$address_space" --fsize="$file_size" \
                  --cpu="$cpu_seconds" --nproc="$processes" \
                      dot -Tpng -Gdpi=192

    # For testing, to verify that network access is indeed blocked:
    #                 nc.traditional -v -v 127.0.0.1 8000
Still, this is enough code that I'm not sure I haven't left something out. Still pending: run ImageMagick or netpbm inside the sandbox to convert the PNG file into a PPM or BMP — there have been CVEs in libpng in the past, and of course it's potentially vulnerable to zip bombs.

Of course this is still exposing most of the Linux kernel system call interface, although fortunately not /proc and /dev. And it's still fucking insane that drawing a node-link graph with three nodes requires more virtual memory than my first Linux machine had in total, in which it ran web browsers and recompiled the kernel. But that's a little further down the line.

In Plan 9 you would get an easy way of namespacing by just unmounting some directories.

All those game mashups made recently by LLMs... Give me a port of Firefox on Plan 9, I'll be impressed then.

bwrap needs additive mode; take everything (aside some basics like stdin/out/err acces) by default, then add permissions.
A caution to anyone else who wants to try this: with this setup, an attacker who finds yet another arbitrary-code-execution vulnerability in Graphviz (or libthai, or Harfbuzz, or whatever) can still enumerate all the library versions in your /usr/lib and encode the result in an output image, and under many plausible threat models they could look at the image and find out which versions of which packages you have installed. Something like Podman could maybe help here by providing a default uninformative set of packages. I wasn't sure the combination would work, but I just verified that this does run successfully inside of my Podman setup here (podman run -it --rm debian:bookworm, then installing bubblewrap):

  bwrap --unshare-user --ro-bind /bin /bin --ro-bind /lib /lib \
    --ro-bind /lib64 /lib64 /bin/sh
Why not just run the official container image? https://hub.docker.com/r/graphviz/graphviz
Docker/Podman isn't really intended for containing malicious code. Bubblewrap is, but I'm not sure how good it is at the job.
Bubblewrap and Podman use the same primitives.
Yes, and it's possible that Podman might accidentally succeed in creating a container that's secure against malicious code running inside of it as a result, but I wouldn't count on it. For that matter, I'm not extremely optimistic about Bubblewrap's chances, either, especially given the experiment described in https://blog.trailofbits.com/2026/08/26/vms-wont-contain-cyb....
I discovered proot-distro build yesterday, which does not require a special kernel with network and pid namespaces, it could run on older machines that do not have those features, or as a unix user that don't have those permissions.

https://pypi.org/project/proot-distro/

The takeaway from this is that (the most popular impmementation of) containers have been birthed from linux primitives, and to the extent they weren't primitives were developed to fulfill those needs.

So, like many linux userspace applications, containers in linux are just a thin wrapper over kernel functionality.