-
Notifications
You must be signed in to change notification settings - Fork 321
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Introduce mkosi-sandbox and stop using subuids for image builds #2956
Conversation
01388f7
to
e659084
Compare
I just attempted to build my image with this and immediatly ran into a problem:
Seems it doesn't copy/use the target of |
@NekkoDroid Should be fixed, please try again. |
61dc66f
to
6d1eb0b
Compare
Indeed the Now I was able to build the image successfully. |
e5848d7
to
6df286f
Compare
I like the idea of not having to deal with bubblewrap but I am not so sure about dropping uid mapping. I fear that most distros are not there yet and for any larger image this would mean having to create dozens of sd-tmpfile configs (and likely still missing a few leading to hard to troubleshoot bugs). Based on the motivation these seem like independent things so maybe it would be better to first adopt mkosi-cage and leave the uid mapping in place until a later time |
0d449d9
to
14b4bcd
Compare
d57f975
to
8c41c6b
Compare
dd66daa
to
c52724b
Compare
7bcfb93
to
d5d5b68
Compare
binary: Optional[PathString], | ||
vartmp: bool = False, | ||
options: Sequence[PathString] = (), | ||
) -> AbstractContextManager[list[PathString]]: ... |
Check notice
Code scanning / CodeQL
Statement has no effect Note
f.write(f"{uid} {os.getuid()} 1\n".encode()) | ||
except OSError as e: | ||
os._exit(e.errno) | ||
except BaseException: |
Check notice
Code scanning / CodeQL
Except block handles 'BaseException' Note
a1b6639
to
9b16f3c
Compare
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). - We had to introduce a Mount struct to pass around mounts so we could deduplicate and sort them before passing them to bubblewrap as bubblewrap did not do this itself. - 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 mkosi-sandbox. 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've 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. - We can get rid of the Mount struct as mkosi-sandbox dedups and sorts operations itself. 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.
Over the last years, we've accumulated a rather nasty set of workarounds
for various issues in bubblewrap:
bubblewrap does not support making its child process the foreground
process.
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.
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.
allow running mkosi in the initramfs as bubblewrap does not fall back to
MS_MOVE if pivot_root() doesn't work.
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.
supported by bubblewrap (even though a PR for the feature has been open for
years).
and sort them before passing them to bubblewrap as bubblewrap did not do this
itself.
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 mkosi-sandbox. 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've 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:
caller.
simple os.getuid()/os.getgid()
all files written by mkosi.
build. Root user builds will now use the system cache instead of the per user
cache.
itself.
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.