It's also literally one line of code in most UNIX-based languages (syscall.Setuid(<uid that isn't root>) in Go, FWIW) to drop root privileges before doing something unsafe. Even if the main Docker daemon absolutely has to run as root most of the time, it can and should fork and drop that access for anything dealing with moving data between unstrusted (e.g. the Internet, user input, etc.) and trusted (verified, read-only local state) security domains.
It's also a perfect example of why even really amazing teams reinventing a language/tooling ecosystem from scratch stumble over problems that were solved years (or even decades ago) in preceding platforms. I leave it as an exercise for the reader to decide if the "reinvent from scratch" critique is more deserved by Docker, Go, or Linux.
That being said, I'm pretty sure even the broken Setuid behavior described there would be good enough to sandbox a thread or child proc that was just handling buffered I/O into and out of the xz binary.
I like a lot of things about Go, but I wish they didn't invent their own mini operating system in the runtime on top of Linux/POSIX. This bug seems like a good example of why that's a leaky abstraction.
I think it would be interesting to explore a coroutines + threads + channels design space for a concurrent language. Basically like Go, except without the green thread implementation.
For CPU bound work, you can use threads. For I/O bound work, use coroutines. And then they all compose together with channels. The implementation would be a lot simpler because it works with the OS rather than trying to paper over it.
Linux actually maintains the uid/euid/suid/gid/egid/sgid/etc. fields per OS thread (which are actually processes, just with a bit of shared memory). The raw syscalls only change the fields on a single task.
Glibc is where the logic happens to propagate that setting to all threads, by setting up signal handlers and immediately triggering a signal, IIRC.
You can get the useful behaviour by using cgo and importing setresuid from unistd.h.
What go should probably do here is:
1) Add os.Set{res,re,}{u,g}id, which implements the logic from glibc.
2) Remove syscall.Set{res,re,}{u,g}id. Anyone that wants that behaviour can use syscall.Syscall6 and syscall.SYS_SETUID or whatever. At least, they could add some really loud godoc to those methods.
The main problem with that is that `os` tries to have a cross-platform API, and (for example) BSD has no saved user or group IDs and therefore no setres{u,g}id. I suspect Windows and Plan9 are even weirder.
It is a bug on Linux -- strictly speaking. Did you see how it is patched? They removed setuid from linux: https://codereview.appspot.com/106170043
> "That these functions should made to fail rather than succeed in their broken state."