core/automount: tear down autofs when the unit vanished across reload (#43705)

This commit is contained in:
Daan De Meyer
2026-09-11 13:36:10 +02:00
committed by GitHub
2 changed files with 339 additions and 14 deletions
+119 -14
View File
@@ -48,6 +48,8 @@ static int automount_dispatch_io(sd_event_source *s, int fd, uint32_t events, vo
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));
@@ -59,7 +61,63 @@ static void automount_init(Unit *u) {
UNIT(a)->ignore_on_isolate = true;
}
static void unmount_autofs(Automount *a) {
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. */
static void automount_release_requests(Automount *a, int status) {
int r;
assert(a);
r = automount_send_ready(a, a->tokens, status);
if (r < 0)
log_unit_warning_errno(UNIT(a), r, "Failed to release pending automount requests, ignoring: %m");
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) {
int r;
assert(a);
@@ -67,27 +125,30 @@ static void unmount_autofs(Automount *a) {
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 we reload/reexecute things we keep the mount point around */
if (!IN_SET(UNIT(a)->manager->objective, MANAGER_RELOAD, MANAGER_REEXECUTE)) {
automount_send_ready(a, a->tokens, -EHOSTDOWN);
automount_send_ready(a, a->expire_tokens, -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");
}
}
static bool automount_keep_mount_point(Automount *a) {
assert(a);
/* If we reload/reexecute things we keep the mount point around, the reloaded unit picks it up again. */
return IN_SET(UNIT(a)->manager->objective, MANAGER_RELOAD, MANAGER_REEXECUTE);
}
static void automount_done(Unit *u) {
Automount *a = ASSERT_PTR(AUTOMOUNT(u));
unmount_autofs(a);
unmount_autofs(a, automount_keep_mount_point(a));
a->where = mfree(a->where);
a->extra_options = mfree(a->extra_options);
@@ -257,7 +318,7 @@ static void automount_set_state(Automount *a, AutomountState state) {
automount_stop_expire(a);
if (!IN_SET(state, AUTOMOUNT_WAITING, AUTOMOUNT_RUNNING))
unmount_autofs(a);
unmount_autofs(a, automount_keep_mount_point(a));
if (state != old_state)
log_unit_debug(UNIT(a), "Changed %s -> %s", automount_state_to_string(old_state), automount_state_to_string(state));
@@ -265,6 +326,42 @@ static void automount_set_state(Automount *a, AutomountState state) {
unit_notify(UNIT(a), state_translation_table[old_state], state_translation_table[state], /* reload_success= */ true);
}
/* 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.
* 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;
bool exposed;
if (lstat(a->where, &st) < 0) {
/* Not knowing what is mounted there, giving the autofs up is the safe side */
log_unit_warning_errno(u, errno, "Failed to stat automount point '%s', assuming it is exposed: %m", a->where);
exposed = true;
} else
exposed = S_ISDIR(st.st_mode) && st.st_dev == a->dev_id;
log_unit_warning(u, "Unit is not loaded anymore (%s), %s automount point '%s'.",
unit_load_state_to_string(u->load_state),
exposed ? "tearing down" : "abandoning", a->where);
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. 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);
/* Nobody can act on a failed unit without a file, so do not keep it around as one, like the orphans
* manager_deserialize() creates for vanished unit files. */
u->collect_mode = COLLECT_INACTIVE_OR_FAILED;
automount_enter_dead(a, AUTOMOUNT_FAILURE_RESOURCES);
return 0;
}
static int automount_coldplug(Unit *u) {
Automount *a = ASSERT_PTR(AUTOMOUNT(u));
int r;
@@ -286,6 +383,9 @@ static int automount_coldplug(Unit *u) {
assert(a->pipe_fd >= 0);
if (u->load_state != UNIT_LOADED)
return automount_coldplug_vanished(a);
r = sd_event_add_io(u->manager->event, &a->pipe_event_source, a->pipe_fd, EPOLLIN, automount_dispatch_io, u);
if (r < 0)
return r;
@@ -458,6 +558,11 @@ static int automount_send_ready(Automount *a, Set *tokens, int status) {
if (set_isempty(tokens))
return 0;
/* We have nothing to talk to the autofs with, e.g. because open_dev_autofs() failed at coldplug and
* left us with the deserialized pipe and tokens. Report that instead of asserting below. */
if (UNIT(a)->manager->dev_autofs_fd < 0 || !a->where)
return -EBADF;
ioctl_fd = open_ioctl_fd(UNIT(a)->manager->dev_autofs_fd, a->where, a->dev_id);
if (ioctl_fd < 0)
return ioctl_fd;