Christian made this possible in Linux 6.15 with a new system call
open_tree_attr() that combines open_tree() and mount_setattr().
Because idmapped mounts are (rightfully) not nested, we have to do
some extra shenanigans to make source we're putting the right source
uid in the userns for any idmapped mounts that we do in nspawn.
Of course we also add the necessary boilerplate to make open_tree_attr()
available in our code and wrap open_tree_attr() and the corresponding
fallback in a new function which we then use everywhere else.
This replaces make_mount_point_inode_from_stat() by
make_mount_point_inode_from_mode() and makes it take a single mode_t
rather than a "struct stat". Moreover, at an "atfd" style directory
parameter.
Then port all users over to new feature, and in particular make use of
the directory fd: use chase() to create and pin parent directories first where
needed.
This adds a new "foreign" value to --private-users-ownership= which is a
lot like "map", but maps from the host's foreign UID range rather than from the
host's 0.
(This has nothing much to do with making unprivileged directory-based
containers work, it's just very handy that we can run privileged
contains with such a mapping too, with an easy switch)
These operations might require slow I/O, and thus might block PID1's main
loop for an undeterminated amount of time. Instead of performing them
inline, fork a worker process and stash away the D-Bus message, and reply
once we get a SIGCHILD indicating they have completed. That way we don't
break compatibility and callers can continue to rely on the fact that when
they get the method reply the operation either succeeded or failed.
To keep backward compatibility, unlike reload control processes, these
are ran inside init.scope and not the target cgroup. Unlike ExecReload,
this is under our control and is not defined by the unit. This is necessary
because previously the operation also wasn't ran from the target cgroup,
so suddenly forking a copy-on-write copy of pid1 into the target cgroup
will make memory usage spike, and if there is a MemoryMax= or MemoryHigh=
set and the cgroup is already close to the limit, it will cause an OOM
kill, where previously it would have worked fine.
owneridmap bind option will map the target directory owner from inside the
container to the owner of the directory bound from the host filesystem.
This will ensure files and directories created in the container will be owned
by the directory owner of the host filesystem. All other users will remain
unmapped. Files to be written as other users in the container will not be
allowed.
Resolves: #27037
If we have a DDI that contains only a /usr/ tree (and which is thus
combined with a tmpfs for root on boot) we previously would try to apply
idmapping to the tmpfs, but not the /usr/ mount. That's broken of
course.
Fix this by applying it to both trees.
Instead of mounting over, do an atomic swap using mount beneath, if
available. This way assets can be mounted again and again (e.g.:
updates) without leaking mounts.
Let's add two new helpers: mount_credentials_fs() and
credentials_fs_mount_flags(). The former mounts a file system suitable
for storing of unencrypted credentials at runtime (i.e. a ramfs or
tmpfs). The latter determines the right mount flags to use for such a
mount.
Both functions mostly just take code from execute.c, but make two
changes:
1. If the kernel supports it we'll use a tmpfs with the new "noswap"
mount option instead of ramfs. Was added in kernel 6.4, hence is very
recent, but tmpfs is so much less crappy than ramfs, hence worth it.
2. We'll set MS_NOSYMFOLLOW on the mounts if supported. These file
systems should only contain regulra files, hence no need to allow
symlinks.
There's no need to fchdir() out of the rootfs and back into it around
the umount2(), hence don't.
This brings the logic closer to what the pivot_root() man page suggests.
While we are at it, always operate based on fds, once we opened the
original dir, and pass the path string along only for generating
messages (i.e. as "decoration").
Add tests for both code paths: the pivot_root() one and the MS_MOUNT.
This will make things a bit longer for now, but more powerful as we can
reuse the userns fd between calls to remount_idmap() if we need to
adjust multiple mounts.
No change in behaviour, just some minor refactoring.
This is useful when creating a new network namespace. Unlike procfs,
we need to remount sysfs, otherwise properties of the network interfaces
in the main network namespace are still accessible through the old sysfs,
e.g. /sys/class/net/eth0. All sub-mounts previously mounted on the sysfs
are moved onto the new sysfs mount.
The function will be used in later commits.
If the flag is set, we mount /tmp/ in a way that is suitable for generators and
other quick jobs.
Unfortunately I had to move some code from shared/mount-util.c to
basic/mountpoint-util.c. The functions that are moved are very thin wrappers
around mount(2), so this doesn't actually change much in the code split between
libbasic and libshared.
Implications for the host would be weird if a private mount namespace is not
used, so assert on FORK_NEW_MOUNTNS when the flag is used.
In order to support pivot_root() we need to move mount propagation
changes after the pivot_root(). While MS_MOVE requires the source mount
to not be a shared mount pivot_root() also requires the target mount to
not be a shared mount. This guarantees that pivot_root() doesn't leak
any mounts.
Signed-off-by: Christian Brauner (Microsoft) <brauner@kernel.org>
Currently, services use mount_move_root() in order to setup the root
directory of services using a mount namespace. This relies on MS_MOVE
and chroot(). However, this has serious drawbacks even for relatively
simple mount propagation scenarios.
What systemd currently does is roughly equivalent to the following shell
code:
unshare --mount --propagation=shared
cd /
mount --make-rslave /
mkdir /new-root
mount --rbind / /new-root
cd /new-root
mount --move /new-root /
chroot .
This looks simple enough but has the consequence that two separate mount
trees exist for the lifetime of the service. The first one was created
when the mount namespace was created, and the second one when a new
mount for the rootfs was created. The first mount tree sticks around as
a shadow mount tree. Both mount trees are dependent mounts with the host
rootfs as their dominating mount.
Now, when mount propagation is triggered by the host by e.g.,
mount --bind /opt /mnt
it means that two propagation events are generated. I'm skipping over
the exact kernel details as they aren't that important. The gist is that
for every propagation event that is generated a second one is generated
for the shadow mount tree. In other words, the kernel creates two copies
for each mount that is propagated instead of one.
This isn't necessary. We can simply change the sequence above to:
unshare --mount --propagation=shared
cd /
mount --make-rslave /
mkdir /new-root
# stash fd to old rootfs
# stash fd to new rootfs
mount --rbind / /new-root
mkdir /new-root
cd /new-root
pivot_root . .
# new root is tucked under old root
# chdir into old rootfs via stashed fd
umount -l /old-root
The pivot_root allows us to get rid of the old mount tree that was
created when the mount namespace was created. So after this sequence
only one mount tree is alive. Plus, it's safer and nicer. Moving mounts
isn't pleasnt.
This patch doesn't convert nspawn yet as the requirements are more
tricky given that it wants to preserve the rootfs as a shared mount
which goes against pivot_root() requirements.
Signed-off-by: Christian Brauner (Microsoft) <brauner@kernel.org>
rootidmap bind option will map the root user from the container to the
owner of the mounted directory on the filesystem. This will ensure files
and directories created by the root user in the container will be owned
by the directory owner on the filesystem. All other user will remain
unmapped.
This enum should be used to define various idmapping modes for bind
mounts which might be incompatible. Changing its name and the values
name to reflect that.
Follow-up for 4c733d3046.
Finding a suitable limit that would fit any use cases out there is pretty hard
and since /dev is only writeable by root anyway, let's simply drop the limit.
When using user namespaces in conjunction with uidmapped mounts, nspawn
so far set up two uidmappings:
1. One that is used for the uidmapped mount and that maps the UID range
0…65535 on the backing fs to some high UID range X…X+65535 on the
uidmapped fs. (Let's call this mapping the "mount mapping")
2. One that is used for the userns namespace the container payload
processes run in, that maps X…X+65535 back to 0…65535. (Let's call
this one the "process mapping").
These mappings hence are pretty much identical, one just moves things up
and one back down. (Reminder: we do all this so that the processes can
run under high UIDs while running off file systems that require no
recursive chown()ing, i.e. we want processes with high UID range but
files with low UID range.)
This creates one problem, i.e. issue #20989: if nspawn (which runs as
host root, i.e. host UID 0) wants to add inodes to the uidmapped mount
it can't do that, since host UID 0 is not defined in the mount mapping
(only the X…X+65536 range is, after all, and X > 0), and processes whose
UID is not mapped in a uidmapped fs cannot create inodes in it since
those would be owned by an unmapped UID, which then triggers
the famous EOVERFLOW error.
Let's fix this, by explicitly including an entry for the host UID 0 in
the mount mapping. Specifically, we'll extend the mount mapping to map
UID 2147483646 (which is INT32_MAX-1, see code for an explanation why I
picked this one) of the backing fs to UID 0 on the uidmapped fs. This
way nspawn can creates inode on the uidmapped as it likes (which will
then actually be owned by UID 2147483646 on the backing fs), and as it
always did. Note that we do *not* create a similar entry in the process
mapping. Thus any files created by nspawn that way (and not chown()ed to
something better) will appear as unmapped (i.e. as overflowuid/"nobody")
in the container payload. And that's good. Of course, the latter is
mostly theoretic, as nspawn should generally chown() the inodes it
creates to UID ranges that actually make sense for the container (and we
generally already do this correctly), but it#s good to know that we are
safe here, given we might accidentally forget to chown() some inodes we
create.
Net effect: the two mappings will not be identical anymore. The mount
mapping has one entry more, and the only reason it exists is so that
nspawn can access the uidmapped fs reasonably independently from any
process mapping.
Fixes: #20989
Even though ISO C11 doesn't mandate in which order the type specifiers
should appear, having `unsigned` at the beginning of each type
declaration feels more natural and, more importantly, it unbreaks
Coccinelle, which has a hard time parsing `long unsigned` and others:
```
init_defs_builtins: /usr/lib64/coccinelle/standard.h
init_defs: /home/mrc0mmand/repos/systemd/coccinelle/macros.h
HANDLING: src/shared/mount-util.c
: 1: strange type1, maybe because of weird order: long unsigned
```
Most of the codebase already "complies", so let's fix the remaining
"offenders".
Follow-up for 7d85383edb.
Apparently the previous limit set on the max number of inodes for /dev was too
small as a system with 4096 LUNs attached can consume up to 95k inodes for
symlinks:
# /bin/df -i
Filesystem Inodes IUsed IFree IUse% Mounted on
devtmpfs 49274377 95075 49179302 1% /dev
Hence this patch bumps the limit from 64k to 128k although the new limit is
still pretty arbitrary (that said, not sure if it really makes sense to put
such absolute limit number).
Let's move things around a bit, and open /proc/self/mountinfo if needed
inside of bind_remount_one_with_mountinfo(). That way bind_remount_one()
can become a superthin inline wrapper around
bind_remount_one_with_mountinfo(). Main benefit is that we don't even
have to open /p/s/mi in case mount_setattr() actually worked for us.