Bug 512635

Summary: Free Space Notifier keeps automount partitions mounted forever (e.g. /boot, /efi)
Product: [Plasma] plasmashell Reporter: Tomi Belan <tomi.belan>
Component: generalAssignee: Plasma Bugs List <plasma-bugs-null>
Status: REPORTED ---    
Severity: normal CC: kde
Priority: NOR    
Version First Reported In: 6.5.3   
Target Milestone: 1.0   
Platform: unspecified   
OS: Linux   
Latest Commit: Version Fixed/Implemented In:
Sentry Crash Report:

Description Tomi Belan 2025-11-26 08:59:36 UTC
SUMMARY

If you have an automount partition (autofs) with an idle timeout longer than 1 minute, it will never be unmounted.

It's caused by a timer in freespacenotifier that checks used space every 1 minute, which resets the kernel's timeout.

A specific example is the EFI System Partition (ESP) and Extended Boot Loader Partition (XBOOTLDR). By default, systemd-gpt-auto-generator generates automount units for them on /boot and/or /efi with a 2 minute idle timeout (see LOADER_PARTITION_IDLE_USEC). Lennart's blog says this is intended to improve data safety by limiting how long it can be in a "dirty" state. That benefit is lost if it stays mounted.

STEPS TO REPRODUCE

1. Configure systemd with an automount unit. Options:
a) systemd-gpt-auto-generator creates boot.automount and/or efi.automount if its conditions are met.
b) Manually edit /etc/fstab and add or change a line like this: "/dev/foo /foobar vfat defaults,nosuid,nodev,noexec,noatime,x-systemd.automount,x-systemd.idle-timeout=120 0 2"
c) Manually create /etc/systemd/system/foobar.automount with the required options and "TimeoutIdleSec=120s"

2. Access the partition, e.g. "ls /foobar". At this point, "journalctl" will print: "foobar.automount: Got automount request for /foobar, triggered by 12345 (ls)". (Side note - the ESP is usually touched by systemd-boot-random-seed.service during bootup.)

3. Wait at least 2 minutes.

OBSERVED RESULT

Once the partition is mounted (on first access), it won't automatically unmount.
"journalctl" won't print "Unmounted foobar.mount - /foobar.".
"findmnt" or "mount" will print two lines for /foobar, one with type "autofs" and one with the real fs type, which means it is mounted.

EXPECTED RESULT

It should unmount after the configured idle timeout. Or some longer but finite time.

If possible, Free Space Notifier should still notify when the ESP is getting too full, because it can be a useful warning. Instead of outright excluding automounts, it would be best to find a way to measure free space without refreshing the mount's idle timer.

I included a strace snippet below. Theory: Maybe if the openat() used O_PATH (or was avoided), and the FS_IOC_GETFSLABEL call was avoided, would that be sufficient?

SOFTWARE/OS VERSIONS

Operating System: Arch Linux 
KDE Plasma Version: 6.5.3
KDE Frameworks Version: 6.20.0
Qt Version: 6.10.0
Kernel Version: 6.17.8-arch1-1 (64-bit)
Graphics Platform: Wayland
...

kded 6.20.0-1
plasma-workspace 6.5.3-1
systemd 258.2-2

ADDITIONAL INFORMATION

https://0pointer.net/blog/linux-boot-partitions.html
https://docs.kernel.org/filesystems/autofs.html
https://www.freedesktop.org/software/systemd/man/latest/systemd.automount.html
https://www.freedesktop.org/software/systemd/man/latest/systemd.mount.html
https://www.freedesktop.org/software/systemd/man/latest/systemd-gpt-auto-generator.html
https://github.com/KDE/plasma-workspace/blob/445f6fb07091902d54276c1a233f778a8e258923/freespacenotifier/freespacenotifier.cpp#L37
https://github.com/systemd/systemd/blob/2ba910ab06e5970e9e2dc6d28a8d38a4adcd4a16/src/gpt-auto-generator/gpt-auto-generator.c#L67

I used "fatrace" to figure out which process is the culprit. (But beware, "fatrace" itself also keeps the mount busy.)

If I disable/stop Free Space Notifier in "kcmshell6 kcm_kded", (and stop "fatrace"), the partition unmounts.

strace:

$ sudo strace -f -p $(pidof kded6)
...
[pid  1782] ppoll([{fd=40, events=POLLIN|POLLOUT}], 1, NULL, NULL, 8) = 1 ([{fd=40, revents=POLLOUT}])
[pid 50382] ppoll([{fd=39, events=POLLIN}], 1, NULL, NULL, 8 <unfinished ...>
[pid  1782] write(40, "     f_5f_\0\0\0\vfile:///efi", 25 <unfinished ...>
[pid 50382] <... ppoll resumed>)        = 1 ([{fd=39, revents=POLLIN}])
[pid  1782] <... write resumed>)        = 25
[pid 50382] ioctl(39, FIONREAD <unfinished ...>
[pid  1782] write(5, "\1\0\0\0\0\0\0\0", 8 <unfinished ...>
[pid 50382] <... ioctl resumed>, [25])  = 0
[pid  1782] <... write resumed>)        = 8
[pid  1782] ppoll([{fd=5, events=POLLIN}, {fd=19, events=POLLIN}, {fd=22, events=POLLIN}, {fd=34, events=POLLIN}, {fd=36, events=POLLIN}, {fd=40, events=POLLIN}, {fd=43, events=POLLIN}, {fd=62, events=POLLIN}, {fd=72, events=POLLIN}, {fd=78, events=POLLIN}, {fd=81, events=POLLIN}, {fd=82, events=POLLIN}], 12, {tv_sec=30, tv_nsec=993000000}, NULL, 8 <unfinished ...>
[pid 50382] read(39, "     f_5f_\0\0\0\vfile:///efi", 25) = 25
[pid  1782] <... ppoll resumed>)        = 1 ([{fd=5, revents=POLLIN}], left {tv_sec=30, tv_nsec=992993736})
[pid 50382] statfs("/efi" <unfinished ...>
[pid  1782] read(5, "\4\0\0\0\0\0\0\0", 8) = 8
[pid 50382] <... statfs resumed>, {f_type=MSDOS_SUPER_MAGIC, f_bsize=4096, f_blocks=261628, f_bfree=182207, f_bavail=182207, f_files=0, f_ffree=0, f_fsid={val=[0x10301, 0]}, f_namelen=1530, f_frsize=4096, f_flags=ST_VALID|ST_NOSUID|ST_NODEV|ST_NOEXEC|ST_NOATIME}) = 0
[pid  1782] ppoll([{fd=5, events=POLLIN}, {fd=19, events=POLLIN}, {fd=22, events=POLLIN}, {fd=34, events=POLLIN}, {fd=36, events=POLLIN}, {fd=40, events=POLLIN}, {fd=43, events=POLLIN}, {fd=62, events=POLLIN}, {fd=72, events=POLLIN}, {fd=78, events=POLLIN}, {fd=81, events=POLLIN}, {fd=82, events=POLLIN}], 12, {tv_sec=30, tv_nsec=993000000}, NULL, 8 <unfinished ...>
[pid 50382] readlink("/efi", 0x7f303970f030, 1023) = -1 EINVAL (Invalid argument)
[pid 50382] openat(AT_FDCWD, "/proc/self/mountinfo", O_RDONLY|O_CLOEXEC) = 37
[pid 50382] statx(37, "", AT_STATX_SYNC_AS_STAT|AT_NO_AUTOMOUNT|AT_EMPTY_PATH, STATX_ALL, {stx_mask=STATX_BASIC_STATS|STATX_MNT_ID, stx_attributes=0, stx_mode=S_IFREG|0444, stx_size=0, ...}) = 0
[pid 50382] statx(37, "", AT_STATX_SYNC_AS_STAT|AT_NO_AUTOMOUNT|AT_EMPTY_PATH, STATX_ALL, {stx_mask=STATX_BASIC_STATS|STATX_MNT_ID, stx_attributes=0, stx_mode=S_IFREG|0444, stx_size=0, ...}) = 0
[pid 50382] statx(37, "", AT_STATX_SYNC_AS_STAT|AT_NO_AUTOMOUNT|AT_EMPTY_PATH, STATX_ALL, {stx_mask=STATX_BASIC_STATS|STATX_MNT_ID, stx_attributes=0, stx_mode=S_IFREG|0444, stx_size=0, ...}) = 0
[pid 50382] read(37, "69 2 0:34 / / rw,noatime shared:"..., 16384) = 2989
[pid 50382] read(37, "", 13395)         = 0
[pid 50382] read(37, "", 16384)         = 0
[pid 50382] close(37)                   = 0
[pid 50382] openat(AT_FDCWD, "/efi", O_RDONLY|O_CLOEXEC) = 37
[pid 50382] statx(37, "", AT_STATX_SYNC_AS_STAT|AT_NO_AUTOMOUNT|AT_EMPTY_PATH, STATX_MNT_ID, {stx_mask=STATX_BASIC_STATS|STATX_MNT_ID, stx_attributes=STATX_ATTR_MOUNT_ROOT, stx_mode=S_IFDIR|0755, stx_size=4096, ...}) = 0
[pid 50382] ioctl(37, FS_IOC_GETFSLABEL, 0x7f30397104d0) = -1 ENOTTY (Inappropriate ioctl for device)
[pid 50382] openat(AT_FDCWD, "/dev/disk/by-label", O_RDONLY|O_NONBLOCK|O_CLOEXEC|O_DIRECTORY) = 44
[pid 50382] fstat(44, {st_mode=S_IFDIR|0755, st_size=80, ...}) = 0
[pid 50382] getdents64(44, 0x7f3028019920 /* 4 entries */, 32768) = 104
[pid 50382] newfstatat(AT_FDCWD, "/dev/disk/by-label/ESP", {st_mode=S_IFBLK|0660, st_rdev=makedev(0x103, 0x1), ...}, 0) = 0
[pid 50382] close(44)                   = 0
[pid 50382] close(37)                   = 0
[pid 50382] write(38, "\1\0\0\0\0\0\0\0", 8) = 8
[pid 50382] ppoll([{fd=39, events=POLLIN|POLLOUT}], 1, NULL, NULL, 8) = 1 ([{fd=39, revents=POLLOUT}])
[pid 50382] write(39, "    56_1b_\0\0\0\2\0\0\0\22\0a\0v\0a\0i\0l\0a\0b"..., 96) = 96
[pid 50382] write(38, "\1\0\0\0\0\0\0\0", 8) = 8
[pid 50382] write(38, "\1\0\0\0\0\0\0\0", 8 <unfinished ...>

The heavy artillery - just in case:

sudo bpftrace -e 'kprobe:autofs_direct_busy { printf("afdb start\n"); } kretprobe:autofs_direct_busy { printf("afdb %d\n", retval); } fentry:vmlinux:may_umount_tree { printf("mut %p\n", args.m); } kretprobe:may_umount_tree { printf("mut %d\n", retval); } fentry:vmlinux:mnt_get_count { printf("mgc %p %s\n", args.mnt, str(args.mnt->mnt_devname)); } kretprobe:mnt_get_count { printf("mgc %d\n", retval); } fentry:vmlinux:mntget { let $smnt = (struct mount*)(((uint64)args.mnt) - offsetof(struct mount, mnt)); if (strncmp(str($smnt->mnt_devname), "/dev/", 5) == 0) { printf("mntget %p %s %d %s\n", $smnt, str($smnt->mnt_devname), pid, comm); } }'