Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Stop using bubblewrap and subuids for image builds
Over the last years, we've accumulated a rather nasty set of workarounds for various issues in bubblewrap: - We contributed setpgid to util-linux and use it if available because bubblewrap does not support making its child process the foreground process. - We added the innerpid logic to run() because bubblewrap does not forward signals to the separate child process it runs in the sandbox which meant they were getting SIGKILLed when we killed bubblewrap, preventing proper cleanup from happening. - bubblewrap does not provide a proper way to detect whether the command was found in the sandbox or not, which meant we had to execute command -v within the sandbox separately to check whether the command exists or not. - We had to add extra logic to make sure / was a mount in the initramfs to allow running mkosi in the initramfs as bubblewrap does not fall back to MS_MOVE if pivot_root() doesn't work. - We had to stitch together shell invocations after bubblewrap but before executing the actual command we want to run to make sure directories had the correct mode as bubblewrap creates everything with mode 0700 which was too restrictive in many cases for us. This was fixed with new --perms and --chmod options in bubblewrap 0.5 but we had to keep compat with 0.4 because that's what's shipped in CentOS Stream 9. - We had to figure out a shell hack to do overlayfs mounts as these are not supported by bubblewrap (even though a PR for the feature has been open for years). - Debugging all the above was made all the harder by the fact that bubblewrap's source code is full of tech debt from its history of being a setuid tool instead of using user namespaces. Getting any fixes into upstream is almost impossible as the tool is practically unmaintained. Aside from bubblewrap, our other source of troubles has been newuidmap/newgidmap. Running as a user within the subuid range configured in /etc/sub{u,g}id has meant we're constantly fixing ownership and permissions issues where stuff needs to be chowned and chmodded everywhere to make sure the current user and the subuid user can access the proper files. Another unfortunate side effect is that users end up with many files owned by the subuid root user in their home directories when building images with mkosi; Let's fix all these issues at once by getting rid of bubblewrap and newuidmap/newgidmap. bubblewrap is replaced with a new tool in mkosi/cage.py. It looks and behaves a lot like bubblewrap, except it's much less code and much more flexible to fit our needs, allowing us to get rid of all the hacks we built up over the years to work around issues that didn't get fixed in bubblewrap. To get rid of newuidmap/newgidmap, a rework of our user namespacing was needed. The need to use newuidmap/newgidmap came from the assumption that we need a full 65k subuid range to do unprivileged image builds, as distributions ship packages containing files and directories that are not owned by the root user. After some investigation, it turns out that there's very few files and directories not owned by root in distribution packages if you ignore /var. If we could temporarily ignore the ownership on these files and directories until we can get distributions to only ship root owned files in /usr and /etc of their packages, we could simply map the current user to root in a user namespace and get rid of the subuid range completely. Turns out that's possible with a seccomp filter. seccomp allows you to make all chown() syscalls succeed without actually doing anything. The files and directories end up owned by the root user instead. If we assume this is OK and are OK with instructing users to use tmpfiles to fix up the permissions on first boot if needed, a seccomp filter like this is sufficient to allow us to get rid of doing image builds within a subuid user namespace. It turns out we can go one step further. It turns out that for the majority of the image build, one doesn't actually need to be the root user. Only package managers and systemd-repart need the current user to be mapped to root to do their job correctly. The reason we did the entire build mapped to root until now was that we need to do a few mounts as part of the image build process and for now I was under the assumption that you needed to be root for that. It turns out that when you unshare a user namespace, you get a full set of capabilities regardless of whether you're root or some other uid in the user namespace. The only difference is that when you exec a subprocess as root, the capabilities aren't lost, whereas they are when you exec a subprocess as a non-root user. This can be avoided by adding the capabilities of the non-root user to the inheritable and ambient set. Once that's done, any subprocess exec'd by a non-root user in the user namespace can mount as many bind and overlay mounts as they can think of. The above allows us to run most of the image build under the current user uid instead of root, only switching to root when running package managers, invoking systemd-repart or systemd-tmpfiles, or when chroot-ing into the image. This allows us to get rid of various hacks we had to look up the proper user name or home directory. Specifically, we can get rid of the following: - mkosi-as-caller can become a noop since we now by default run the build as the caller. - Lots of chmod()'s and chown()'s can be removed - All uses of INVOKING_USER.uid/gid can be removed, and most can be replaced with simple os.getuid()/os.getgid() - We can use /etc/passwd and /etc/group from the host instead of building our own - We can get rid of the Acl= option as the user will now be able to remove (almost) all files written by mkosi. - We don't have to rchown the package manager cache directory anymore after each build. Root user builds will now use the system cache instead of the per user cache. One thing to note is that if we're invoked as root, none of the seccomp or capabilities stuff applies and it is all skipped as it's not required in that case. This means that when building as root it's still possible to have more than one user in the generated image unlike when building unprivileged. Also note that users can still be added to /etc/passwd and such, they just can't own any files or directories in the image itself until the image is booted.
- Loading branch information