Many *at()-style helpers accept a directory fd that may be either a
regular, valid fd (>= 0) or one of the special AT_FDCWD/XAT_FDROOT
wildcard values, and open-code that check in their assertions. Add a
wildcard_fd_is_valid() helper for it and use it tree-wide.
Split the single directory fd that chaseat() used to take into two separate
fds: a root_fd that sets the chroot boundary (symlinks may not escape it,
absolute symlinks resolve relative to it), and a dir_fd that path resolution
starts from. This makes the chroot semantics of chaseat() explicit at every
call site instead of encoding them in the CHASE_AT_RESOLVE_IN_ROOT flag,
which is removed. It also decouples the starting directory from the root
boundary, so callers can descend from any inode inside the tree without
having to reopen the root separately.
XAT_FDROOT passed as root_fd means "no containment" (host root); as dir_fd
it means "start at root_fd". For a smoother transition, AT_FDCWD is also
accepted as root_fd and treated as XAT_FDROOT. When root_fd points to a
directory that is actually the host root, it is normalized to XAT_FDROOT
up front so the existing shortcut path can kick in.
Absolute paths returned by chaseat() are now relative to root_fd, and
relative paths are relative to dir_fd. The result is absolute only when
there is no chroot boundary (root_fd is XAT_FDROOT), or when an absolute
symlink made resolution jump out of the dir_fd subtree; otherwise callers
get a relative path they can feed straight back into an openat()-style
call against dir_fd. Specifically, when dir_fd == root_fd and we're not
operating on the host's root directory, we return a relative path even if
we received an absolute path or resolved an absolute symlink to allow
passing the path directly to openat() style functions. We do this to not
have to go modify every caller of chaseat() to make sure they deal properly
with any absolute paths they might receive. Only when root_fd != dir_fd do
we have to return an absolute path to indicate that the path is relative to
root_fd and not dir_fd.
The shortcut that skips the per-component walk is reworked around a new
chase_xopenat() helper that funnels CHASE_NOFOLLOW, CHASE_MUST_BE_* and
CHASE_TRIGGER_AUTOFS through xopenat_full()'s O_NOFOLLOW, O_DIRECTORY,
XO_REGULAR, XO_SOCKET and XO_TRIGGER_AUTOMOUNT flags. As a result these
flags no longer force us off the shortcut and can be dropped from
CHASE_NO_SHORTCUT_MASK, and the old openat_opath_with_automount() helper
goes away. A CHASE_MUST_BE_ANY alias is introduced for shortcut callers
(stat/access paths) that don't go through xopenat_full() and still need
to bail on those flags locally.
All *_and_* helpers built on top of chaseat() (chase_and_openat,
chase_and_opendirat, chase_and_statat, chase_and_accessat,
chase_and_fopenat_unlocked, chase_and_unlinkat, chase_and_open_parent_at)
gain the same root_fd parameter, and every call site in the tree is
ported to the new signature.
chase_and_open() is also fixed to correctly handle CHASE_EXTRACT_FILENAME
without CHASE_PARENT.
Some flags don't reasonably apply to chase_and_unlinkat() (because we
open the parent inode of an inode to delete, which is always a dir),
hence let's catch these flags when misused.
(I ran into this, and it was very confusing to debug, hence let's make
it easier)
So far statx_mount_same() assumed STATX_MNT_ID_UNIQUE didn't exist.
However it does exist, hence do something useful if we see it set. Note
that this creates a certain ambiguity: if we compare one statx struct
with STATX_MNT_ID_UNIQUE and one without it (but with the regular mnt
id), then we cnanot really come to a clear conclusion, hence need to
introduce a third, unknown state.
Note that we don't request STATX_MNT_ID_UNIQUE yet wherever we call
statx_mount_same(). THis will be added in a later commit.
The same optimization is already applied in the very similar
dir_fd_is_root() check a few lines up – with the exception that it
doesn't accept AT_FCWD there. And frankly turning off
CHASE_AT_RESOLVE_IN_ROOT if we operate on AT_FCWD is simply wrong. Hence
remove this code.
Let's harden things in case concurrent access is allowed to a root tree
passed via CHASE_AT_RESOLVE_IN_ROOT: let's not just validate via the
path if we hit the root of the tree, but also by comparing inodes +
mount ids.
Hardening opportunity reported by Sebastian Wick.
In one of the next commits we want to acquire .stx_mnt_id from statx()
for each inode we traverse (plain fstat() doesn't provide that field).
Hence let's port chase() over to statx() as preparation for that.
No change in behaviour.
Let's add some protections in case we deal with inodes owned by an
untrusted person, with concurrent access: let's put a limit on how long
we traverse, and fail eventually so that live changes cannot send us in
circles indefinitely.
This reworks the current CHASE_MAX logic so that it not only applies to
symlinks transitions, but to any transitions.
This also bumps CHASE_MAX a bit, given that it's now bumped on every
single iteration of the loop.
Now that we can recognize the root dir in chaseat() sanely, let's use it
top optimize the very common special case where we have no root dir to
consider, and directly call open_tree().
We pin the parent directory of the specified directory via CHASE_PARENT,
but if we do that we really should mask off CHASE_MUST_BE_REGULAR,
because a parent dir of course is a dir, nothing else. The
CHASE_MUST_BE_REGULAR after all should apply to the file created in that
dir, not to the parent.
Since c5de7b14ae
file searching implies a new mount api syscall by default,
to trigger automounts.
But, this is not necessary in most cases, e.g. when chasing
syspath in sd-device (actually this causes regression in umockdev,
see https://github.com/martinpitt/umockdev/issues/271).
Another example is reading unit files, especially .network files,
as automount may trigger mounting network filesystems...
Also, when this is used in NSS plugins, programs that load the
plugins may fail because of spuriously configured seccomp. See #38565.
Let's not trigger automount by default, and do only when explicitly
requested.
This introduces CHASE_TRIGGER_AUTOFS, and use it in
- service manager,
- bootctl and finding ESP/xbootldr,
- sysupdate,
- mountfsd,
- systemd-mount.
There may be several more places we should trigger automount, but let's
do that later.
Follow-up for c5de7b14ae.
Fixes#38565.
Replaces #38569.
Co-authored-by: Luca Boccassi <luca.boccassi@gmail.com>
This reverts commit 490aa05ca1.
As commented https://github.com/systemd/systemd/pull/38569#discussion_r2284978273,
the commit makes autofs check bypassed. Before the commit, when
CHASE_NO_AUTOFS is set, we did not shortcut chasing paths, and refused
any autofs mount points in the path. However, with the commit, the flag
was swapped but even when CHASE_AUTOFS is unset, the autofs check may be
skipped.
To fix the issue, rather than swapping the flag, we should introduce
another flag, say CHASE_TRIGGER_AUTOFS. This revert the commit, and in a
later commit, the new flag will be introduced.
Since https://github.com/systemd/systemd/commit/c5de7b14ae2e08d267d8d75bc88934ac6aa7dcd6
file searching implies a new mount api syscall by default,
to trigger automounts.
This is problematic in NSS plugins, as they are dlopen'ed inside
processes by glibc, for two reasons.
First of all, potentially searching on a networked filesystem
automount could lead to nasty surprises, such as the process
responsible for setting up the network filesystem trying to
search on that same filesystem.
More importantly, the new mount api syscall was never part of
the filesystem seccomp filter that we provide by default, and
given mounting/remounting/bind mounting is one of the possible
ways to bypass sandboxing it is very likely not allowed when
custom filters are used in sandboxed processes, if they don't
need to do these operations otherwise.
The filesystem seccomp mask we provide has been updated, however
this only takes effect on the next restart of a service. When
systemd is upgraded via a package upgrade, the new nss plugin is
installed and will be immediately dlopen'ed by glibc when needed,
without waiting for the process to restart, which means the existing
seccomp filter applies, causing the filter to trigger.
Given it's not really possible for any arbitrary program to
predict which NSS modules glibc will load, given programs do not
configure that and instead nsswitch is set up by the sysadmin,
it's impossible to handle at each process level. It's also not
possible to know when it will be triggered, given the plugin
is not linked in each binary tools like need-restart cannot
even pre-emptively restart services that may be affected.
This means in practice, upgrading from systemd << v258 to >= v258
requires a reboot to avoid either subtle or catastrophic system
failures.
By avoiding to trigger automounts in nss-systemd we can avoid
both issues.
userdb drop-ins are searched for in:
/etc/userdb/
/run/userdb/
/run/host/userdb/
/usr/local/lib/userdb/
/usr/lib/userdb/
none of which are supported as automounts anyway.
Note that this happens only when the userdbd service is not running,
as otherwise nss-systemd will go through the varlink IPC, rather than
doing the searches in-process.
So invert CHASE_NO_AUTOFS to CHASE_AUTOFS and set it in the places where
we do want to trigger automounts, like looking for the ESP.
Follow-up for https://github.com/systemd/systemd/commit/c5de7b14ae2e08d267d8d75bc88934ac6aa7dcd6
Fixes https://github.com/systemd/systemd/issues/38565
Otherwise, if it is called with CHASE_NONEXISTENT, when we call
stat_verify_directory()/_regular() the struct stat is for one of the
parent directory, rather than for the result path.
With this change, we can safely specify CHASE_MUST_BE_DIRECTORY/REGULAR
with CHASE_NONEXISTENT.
More importantly, chaseat() internally sets CHASE_MUST_BE_DIRECTORY when
the input path ends with "/", "/,", "/..". Hence, without this change,
we cannot specify CHASE_NONEXISTENT safely.
Follow-up for 90b9f7a07e.
Follow-up for c5de7b14ae
chase() is arguably a hot path in our code, hence it deserves
some caching whether open_tree() is available. Moreover,
the manual set of r to -EPERM feels kinda ugly. Let's
instead extract this bit into its own function.
As it turns out open() with O_PATH does *not* trigger autofs, you get a
reference to the autofs inode, if not triggered.
But there's a way out: open_tree() (when specified without
OPEN_TREE_CLONE) is actually fully equivalent to open() with O_PATH –
with the exception of one thing: it *does* trigger automounts.
Thanks for Christian Brauner for pointing me to this and saving my day.
Fixes: #33155
And rename it to empty_or_root_harder_to_null(), as it also checks if
the input path effectively points to the root by calling path_is_root().
This also adds simple test cases for the function.
Admittedly, some of our glyphs _are_ special, e.g. "O=" for SPECIAL_GLYPH_TOUCH ;)
But we don't need this in the name. The very long names make some invocations
very wordy, e.g. special_glyph(SPECIAL_GLYPH_SLIGHTLY_UNHAPPY_SMILEY).
Also, I want to add GLYPH_SPACE, which is not special at all.
let's move final processing of the filename out of the loop, and apply
it in all cases, uniformly, even if we are asked to only return the
final filename.
This also implies the new CHASE_MUST_BE_DIRECTORY flag in case the
specified path ends in a slash. This makes the rules stricter, it means
we'll be closer to how this is handled in kernel: if a path ends in a
slash it can never refer to a non-directory.
This allows using CHASE_MKDIR_0755 without CHASE_NONEXISTENT or
CHASE_PARENT, so that it will create the final component of the path
too should it be missing.
This is really useful as a mkdir_p() replacement that returns an fd to
the final component, and knows how to operate relative to a root fs.
Kinda reverts 4ea0bcb922 (which only
refused the flags combination which didn't work, instead of making it
work, which is what this commit does.)
This also corrects behaviour if CHASE_MKDIR_0755 is used in one more
way: we'll now always open the dir as O_PATH. This is generally the
better idea, but matters in particular once with allow using
CHASE_MKDIR_0755 to create the final component: we should uniformly
return an O_PATH dir that must be converted to a proper fd first before
using it.
Currently the check also succeeds if the input path starts with a dot, whereas
we only want it to succeed for "." and "./". Tighten the check and add a test.
If we use CHASE_PARENT on a path ending in ".." then things are a bit
weird, because we the last path we look at is actually the *parent* and not
the *child* of the preceeding path. Hence we cannot just return the 2nd
to last fd we look at. We have to correct it, by going *two* levels up,
to get to the actual parent, and make sure CHASE_PARENT does what it
should.
Example: for the path /a/b/c chase() with CHASE_PARENT will return
/a/b/c as path, and the fd returned points to /a/b. All good. But now,
for the path /a/b/c/.. chase() with CHASE_PARENT would previously return
/a/b as path (which is OK) but the fd would point to /a/b/c, which is
*not* the parent of /a/b, after all! To get to the actual parent of
/a/b we have to go *two* levels up to get to /a.
Very confusing. But that's what we here for, no?
@mrc0mmand ran into this in https://github.com/systemd/systemd/pull/28891#issuecomment-1782833722
In chaseat() we call dir_fd_is_root() several places, and the final
result depends on it. If the root path specified to `chase()` is not
normalized but points to "/", e.g. "/../", assertions in `chaseat()` or
`chase()` may be triggered.