core/automount: fail unread queued requests when giving up

When an automount goes away, the tokens we answer are only those we have
read from the pipe. Requests the kernel queued after our last read stayed
blocked: closing the pipe does not wake them, the kernel only notices the
closed pipe and turns the autofs catatonic when yet another request fails
to reach us, and after the lazy unmount no request reaches it anymore. In
the normal stop path that window is one event loop iteration; for an
automount torn down at coldplug it spans the whole reload.

Set the autofs to catatonic state before letting go of it, which fails
everything still queued with ENOENT. Answer the known tokens first:
closing our end of the pipe before that made a racing request flip the
kernel into catatonic mode on its own, which discards the queue and
turns our answers into "token not found" errors.

Signed-off-by: Martin Pitt <martin@amutable.com>
This commit is contained in:
Martin Pitt
2026-09-11 08:18:43 +02:00
parent f65e67ff0f
commit bdb302ffe6
2 changed files with 113 additions and 13 deletions
+52 -12
View File
@@ -49,6 +49,7 @@ static int automount_start_expire(Automount *a);
static void automount_stop_expire(Automount *a);
static int automount_send_ready(Automount *a, Set *tokens, int status);
static void automount_enter_dead(Automount *a, AutomountResult f);
static int open_ioctl_fd(int dev_autofs_fd, const char *where, dev_t devid);
static void automount_init(Unit *u) {
Automount *a = ASSERT_PTR(AUTOMOUNT(u));
@@ -60,6 +61,43 @@ static void automount_init(Unit *u) {
UNIT(a)->ignore_on_isolate = true;
}
static int autofs_set_catatonic(int dev_autofs_fd, int ioctl_fd) {
struct autofs_dev_ioctl param;
assert(dev_autofs_fd >= 0);
assert(ioctl_fd >= 0);
init_autofs_dev_ioctl(&param);
param.ioctlfd = ioctl_fd;
return RET_NERRNO(ioctl(dev_autofs_fd, AUTOFS_DEV_IOCTL_CATATONIC, &param));
}
/* Requests the kernel queued since we last read the pipe are in no token set of ours. Turning the autofs
* catatonic fails them with ENOENT; merely closing the pipe would get the kernel there only once yet another
* request fails to reach us, which under a file system mounted on top never happens. Call this after answering
* the known tokens, so that the kernel does not discard them first. */
static void automount_set_catatonic(Automount *a) {
_cleanup_close_ int ioctl_fd = -EBADF;
int r;
assert(a);
/* No way to talk to the autofs, e.g. when open_dev_autofs() failed at coldplug */
if (UNIT(a)->manager->dev_autofs_fd < 0 || !a->where)
return;
ioctl_fd = open_ioctl_fd(UNIT(a)->manager->dev_autofs_fd, a->where, a->dev_id);
if (ioctl_fd < 0) {
log_unit_warning_errno(UNIT(a), ioctl_fd, "Failed to open automount ioctl fd for '%s', ignoring: %m", a->where);
return;
}
r = autofs_set_catatonic(UNIT(a)->manager->dev_autofs_fd, ioctl_fd);
if (r < 0)
log_unit_warning_errno(UNIT(a), r, "Failed to make automount point '%s' catatonic, ignoring: %m", a->where);
}
/* Nothing else can answer the requests the kernel already queued for us, and it keeps their processes
* blocked until we do. Make a failure to do so at least visible. The status is what the mount requests get:
* an expire request can only ever fail. */
@@ -75,6 +113,8 @@ static void automount_release_requests(Automount *a, int status) {
r = automount_send_ready(a, a->expire_tokens, -EHOSTDOWN);
if (r < 0)
log_unit_warning_errno(UNIT(a), r, "Failed to release pending automount expire requests, ignoring: %m");
automount_set_catatonic(a);
}
static void unmount_autofs(Automount *a, bool keep_mount_point) {
@@ -85,17 +125,16 @@ static void unmount_autofs(Automount *a, bool keep_mount_point) {
if (a->pipe_fd < 0)
return;
if (!keep_mount_point)
automount_release_requests(a, -EHOSTDOWN);
a->pipe_event_source = sd_event_source_disable_unref(a->pipe_event_source);
a->pipe_fd = safe_close(a->pipe_fd);
if (!keep_mount_point) {
automount_release_requests(a, -EHOSTDOWN);
if (a->where) {
r = repeat_unmount(a->where, MNT_DETACH|UMOUNT_NOFOLLOW);
if (r < 0)
log_unit_error_errno(UNIT(a), r, "Failed to unmount: %m");
}
if (!keep_mount_point && a->where) {
r = repeat_unmount(a->where, MNT_DETACH|UMOUNT_NOFOLLOW);
if (r < 0)
log_unit_error_errno(UNIT(a), r, "Failed to unmount: %m");
}
}
@@ -289,9 +328,8 @@ static void automount_set_state(Automount *a, AutomountState state) {
/* This automount unit's file vanished while we were reloading. Nobody can serve the autofs mount point
* anymore, but the kernel keeps blocking every process that is trying to access it while the mount exists.
* If the mount is already active, nothing waits for it: then only let go of the pipe. Otherwise detach the
* automount and fail the unit, which is what automount_enter_running() does with a request that arrives
* while the unit is not loaded. */
* Answer whatever is queued and detach the automount, which is what automount_enter_running() does with a
* request that arrives while the unit is not loaded. */
static int automount_coldplug_vanished(Automount *a) {
Unit *u = UNIT(ASSERT_PTR(a));
struct stat st;
@@ -310,7 +348,9 @@ static int automount_coldplug_vanished(Automount *a) {
if (!exposed)
/* Mop up pending requests, nothing else will answer any more. The file system on top is
* what they were waiting for, so tell them it is there. */
* what they were waiting for, so tell them it is there. The autofs below it cannot be
* unmounted and stays behind catatonic: recovering that path takes a manual umount, and
* until then automount_start() refuses to use it again. */
automount_release_requests(a, 0);
unmount_autofs(a, /* keep_mount_point= */ !exposed);
+61 -1
View File
@@ -20,6 +20,10 @@ MOUNT_POINT=/tmp/TEST-07-PID1-issue-43700
UNIT=$(systemd-escape --path "$MOUNT_POINT")
STAT_ERR=/tmp/TEST-07-PID1-issue-43700.stderr
CURSOR=/tmp/TEST-07-PID1-issue-43700.cursor
GENERATOR=/run/systemd/system-generators/TEST-07-PID1-issue-43700
# Not below /tmp: PID 1 runs generators with a private one, so we would never see these
MARKER=/run/TEST-07-PID1-issue-43700.reloading
RELEASE=/run/TEST-07-PID1-issue-43700.release
at_exit() {
set +e
@@ -29,7 +33,7 @@ at_exit() {
while [[ -n "$(fstype "$MOUNT_POINT")" ]]; do
umount -l "$MOUNT_POINT" || break
done
rm -f /run/systemd/system/"$UNIT".{auto,}mount "$STAT_ERR" "$CURSOR"
rm -f /run/systemd/system/"$UNIT".{auto,}mount "$STAT_ERR" "$CURSOR" "$GENERATOR" "$MARKER" "$RELEASE"
systemctl unmask "$UNIT".automount
systemctl daemon-reload
systemctl reset-failed "$UNIT".automount "$UNIT".mount
@@ -158,3 +162,59 @@ journalctl --cursor-file="$CURSOR" --grep "tearing down automount point '$MOUNT_
systemctl unmask "$UNIT".automount
rm /run/systemd/system/"$UNIT".{auto,}mount
systemctl daemon-reload
# A request arriving while PID 1 reloads sits unread in the pipe when the vanished unit is torn down, so
# its token is unknown to PID 1. It must still be failed rather than left blocked. Generators run
# synchronously, so one that waits for us holds the reload open for as long as it takes to get a request
# into that window.
write_automount
write_hanging_mount
systemctl daemon-reload
systemctl start "$UNIT".automount
mkdir -p "$(dirname "$GENERATOR")"
cat >"$GENERATOR" <<EOF
#!/bin/sh
touch $MARKER
# Wait for the test to park its request, but give up eventually rather than hold PID 1 forever if the
# test died before it got there
i=0
while [ ! -e $RELEASE ] && [ \$i -lt 200 ]; do
i=\$((i + 1))
sleep .1
done
EOF
chmod +x "$GENERATOR"
rm /run/systemd/system/"$UNIT".{auto,}mount
rm -f "$MARKER" "$RELEASE"
systemctl daemon-reload &
RELOAD_PID=$!
# Wait for the generator to run: PID 1 dispatches no events until the reload is through, so a request
# made from here on is one it cannot have read. Assuming that instead of waiting for it would make the
# case pass on an answered token, which fails with the same ENOENT.
timeout 30 bash -c "until [[ -e $MARKER ]]; do sleep .1; done"
timeout -k 5 -s KILL 60 stat "$MOUNT_POINT"/x 2>"$STAT_ERR" &
WAITER_PID=$!
# The request is parked in the kernel while the autofs is still armed, i.e. before PID 1 got to the
# teardown, so it was made inside the reload and PID 1 never read it. These waits are unhurried because
# the generator holds the reload until they are through. timeout(1) has to fork stat(1) first, and
# /proc/PID/wchan needs a kernel with CONFIG_KALLSYMS to name the frame.
timeout 10 bash -c "until pgrep -P $WAITER_PID >/dev/null; do sleep .1; done"
STAT_PID=$(pgrep -P "$WAITER_PID")
timeout 10 bash -c "until grep autofs /proc/$STAT_PID/wchan >/dev/null; do sleep .2; done"
[[ "$(fstype "$MOUNT_POINT")" == autofs ]]
touch "$RELEASE"
wait "$RELOAD_PID"
rm "$GENERATOR" "$MARKER" "$RELEASE"
# The kernel fails unread requests with ENOENT once the autofs is catatonic
rc=0
wait "$WAITER_PID" || rc=$?
unset WAITER_PID
[[ $rc -ne 0 && $rc -ne 137 ]]
grep -F 'No such file or directory' "$STAT_ERR"
journalctl --sync
journalctl --cursor-file="$CURSOR" --grep "tearing down automount point '$MOUNT_POINT'"
(! systemctl is-active "$UNIT".automount)
(! systemctl is-failed "$UNIT".automount)
[[ -z "$(fstype "$MOUNT_POINT")" ]]