Commit Graph
87 Commits
Author SHA1 Message Date
Lennart Poettering eb3799a373 fd-util: add wildcard_fd_is_valid() helper and use it tree-wide
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.
2026-05-28 13:33:08 +01:00
Daan De Meyer 8b81e3b751 chase: Use openat2() if available
Let's make use of openat2() if we can in chaseat().
2026-05-12 14:43:39 +02:00
Daan De Meyer 452214c09b chase: Use ELOOP for CHASE_PROHIBIT_SYMLINKS error
Matches the behavior of openat2() with RESOLVE_NO_SYMLINKS
which makes introducing support for openat2() easier.
2026-05-12 14:43:39 +02:00
Daan De Meyer 79862d33a0 chase: add explicit root_fd parameter to chaseat() and drop CHASE_AT_RESOLVE_IN_ROOT
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.
2026-04-20 23:35:31 +02:00
Lennart Poettering 4712d70669 chase: tighten flags checks in chase_and_unlinkat()
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)
2026-04-20 17:19:27 +02:00
Lennart Poettering d2b27a7a8c xstatx_full(): add flag to acquire STATX_MNT_ID_UNIQUE if we can, with fallback. 2026-03-03 08:49:23 +01:00
Lennart Poettering 2e9208199f stat-util: teach statx_mount_same() STATX_MNT_ID_UNIQUE
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.
2026-03-03 08:48:01 +01:00
Lennart Poettering 0384875582 chase: drop wrong optimization
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.
2026-03-03 08:48:01 +01:00
Lennart Poettering 3538b77f9d chase: tighten checks on ".." once we hit the root of an CHASE_AT_RESOLVE_IN_ROOT root tree
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.
2026-03-03 08:48:01 +01:00
Lennart Poettering c8031ff37e chase: port to statx()
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.
2026-03-03 08:48:01 +01:00
Lennart Poettering 004e60e0fc chase: put limit on overall chase cycles
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.
2026-03-03 08:48:01 +01:00
Lennart Poettering 3e4fca7489 chase: add new flag CHASE_MUST_BE_SOCKET
Just like CHASE_MUST_BE_DIRECTORY and CHASE_MUST_BE_REGULAR, but test if
the inode is a socket.
2026-02-09 10:44:47 +01:00
Lennart Poettering c67abc9c43 chase: extend coverage of the root fd shortcut
Let's use dir_fd_is_root() to check if we are looking at the real root
fs.
2026-01-17 12:37:39 +01:00
Lennart Poettering ded2531764 chase: optimize the special case where no root dir specified
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().
2026-01-17 12:37:39 +01:00
Lennart Poettering 874971b1c0 chase: wire up XAT_FDROOT 2026-01-17 12:37:39 +01:00
Yu Watanabe 93378148d4 tree-wide: normalize comment style 2025-12-15 08:50:13 +09:00
Lennart Poettering 0712316e8e fs-util: enable automatic access mode logic in xopenat() 2025-11-18 01:31:49 +09:00
Lennart Poettering 0ab316ff14 chase: fix typo in log message
(While we are at it, add quotes around user provided strings)
2025-11-07 14:04:19 +01:00
Yu Watanabe 542552612b chase: tweaks to chase_open()/chase_openat() (#38984) 2025-09-19 16:11:24 +09:00
Lennart Poettering 93dea63fab fileio: modernize xopendirat() a bit 2025-09-19 08:39:32 +02:00
Lennart Poettering fb7151a350 chase: honour CHASE_MUST_BE_DIRECTORY/CHASE_MUST_BE_REGULAR properly in chase_and_openat() and related calls 2025-09-18 09:36:44 +02:00
Lennart Poettering be1117f712 chase: mask away CHASE_MUST_BE_REGULAR in chase_openat()
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.
2025-09-18 09:36:44 +02:00
Yu WatanabeandLuca Boccassi 4279336618 chase: trigger automount only when explicitly requested
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>
2025-08-20 02:32:02 +09:00
Yu Watanabe 9dcd034085 chase: introduce CHASE_NO_SHORTCUT_MASK 2025-08-20 02:24:39 +09:00
Yu Watanabe 2e7d7e929e Revert "chase: invert CHASE_NO_AUTOFS and only set it where needed"
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.
2025-08-20 02:15:36 +09:00
Luca Boccassi 490aa05ca1 chase: invert CHASE_NO_AUTOFS and only set it where needed
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
2025-08-19 16:48:13 +02:00
Yu Watanabe 1cabb6905b chase: check the result is a directory or regular file only when the resolved path exists
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.
2025-07-29 03:37:48 +09:00
Yu Watanabe 628f45f4a3 chase: allow to request O_PATH fd even with CHASE_NONEXISTENT 2025-07-11 10:39:55 +09:00
Mike Yuan 1320e56283 chase: introduce openat_opath_with_automount() helper
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.
2025-07-05 12:18:55 +02:00
Lennart Poettering c5de7b14ae chase: when chasing paths, trigger automounts
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
2025-07-03 22:18:56 +01:00
Yu Watanabe 8ce463639a path-util: move empty_or_root_to_null() from chase.c
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.
2025-06-30 03:45:29 +09:00
Daan De Meyer 0c15577abe basic + fundamental: Clean up includes
Split out of #37344.
2025-05-25 10:06:07 +02:00
Daan De Meyer d9ccf6b349 path-util: Reduce transitive includes 2025-05-07 22:04:46 +09:00
Daan De Meyer fda652117e fs-util: Reduce transitive includes 2025-05-07 22:04:46 +09:00
Zbigniew Jędrzejewski-Szmek 1ae9b0cfa8 basic/glyph-util: rename "special glyph" to just "glyph"
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.
2025-03-15 14:40:39 +01:00
Lennart Poettering 3351c0a510 chase: move appending of trailing slash out of loop
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.
2025-01-20 11:35:04 +01:00
Lennart Poettering 90b9f7a07e chase: introduce flags that verify that chased inode is regular file or dir
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.
2025-01-20 11:35:03 +01:00
Lennart Poettering b0afd29f7d chase: use streq() not path_equal() to compare filenames 2025-01-20 11:31:55 +01:00
Lennart Poettering e49e76d64d chase: allow using chase() as mkdir_p() replacement
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.
2025-01-20 11:31:55 +01:00
Lennart Poettering 8beb8c3e35 fs-util: teach xopenat_full() to pick automatically if given as MODE_INVALID 2025-01-03 12:58:43 +01:00
Daan De Meyer 87333bd1dc chase: Fix shortcut
We can't shortcut chaseat() if CHASE_PARENT is set.
2024-10-04 21:34:33 +02:00
Daan De Meyer 7efaab482a chase: Tighten "." and "./" check
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.
2024-06-11 13:55:35 +01:00
Zbigniew Jędrzejewski-Szmek 454318d3e8 various: use strdup_to() in various obvious cases
strdup_to() returns 0 on success and here we convert obvious blocks
which either return -ENOMEM or 0.
2024-03-20 15:13:44 +01:00
Yu Watanabe e40b11be04 fs-util: rename xopenat() -> xopanat_full() 2024-02-16 00:25:37 +09:00
Yu Watanabe bec1383623 chase: do not wrap xopenat() with RET_NERRNO()
Follow-up for 47f0e1b5e0.
2024-02-16 00:25:37 +09:00
Lennart Poettering 9c21cfdd7d chase: fix corner case when using CHASE_PARENT with a path ending in ".."
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
2023-11-01 14:43:24 +00:00
Yu Watanabe 4445242a1e chase: propagate error in dir_fd_is_root()
Otherwise, if it fails, chaseat() may return unexpected result and
triggers an assertion in chase().
2023-07-29 22:09:32 +09:00
Yu Watanabe 83c57d8cde chase: check root path in more detail
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.
2023-07-29 22:09:28 +09:00
Yu Watanabe 4de5b4e375 chase: carefully handle result of extracting parent directory
Should not change any behavior.
2023-07-29 21:58:30 +09:00
Yu Watanabe 1c13bdf342 chase: add two more assertions
When 'need_absolute' is true, 'done' should always contain "/" at the
beginning, and thus should not be NULL.
2023-07-29 21:58:30 +09:00