* [PATCH] efivarfs: avoid slow QueryVariableInfo() in statfs()
@ 2026-09-17 7:16 Prashant Singh
2026-09-17 8:36 ` Ard Biesheuvel
0 siblings, 1 reply; 3+ messages in thread
From: Prashant Singh @ 2026-09-17 7:16 UTC (permalink / raw)
To: jk, ardb; +Cc: linux-efi, linux-kernel, corbet, linux-doc, Prashant Singh
QueryVariableInfo() is an EFI runtime service that, on some platforms,
takes tens of milliseconds and runs with preemption disabled, freezing
the CPU that services it for the whole call. efivarfs_statfs() issued
this call on every statfs(2) (e.g. every "df"), which produced large
latency spikes for unrelated latency-sensitive workloads pinned to the
same CPU.
Commit b2326338dc68 ("efivarfs: Rate limit statfs() handler") already
bounds the firmware call to twice per second. That helps against a
statfs() flood, but on a real-time / dataplane system even the residual
2 calls/s x ~40ms = up to ~80ms/s of preempt-disabled CPU time is
unacceptable: any unprivileged process (or a periodic monitoring "df")
can still inject ~40ms stalls into a co-located latency-sensitive task.
Cache the reported capacity instead of querying firmware from the
statfs() hot path:
- The cache is primed once at mount time so df has valid numbers.
- When fs.efivarfs.statfs_refresh is set (default 1, preserving the
current rate-limited behaviour), statfs() refreshes the cache in the
same rate-limited way.
- Latency-sensitive systems can set fs.efivarfs.statfs_refresh to 0 so
statfs() never calls firmware; the cache is then kept up to date only
by mount and explicit fs.efivarfs.force_refresh writes.
- fs.efivarfs.force_refresh is a write-only knob that forces an
immediate synchronous re-query on demand.
On a hard query failure (a status other than EFI_SUCCESS or
EFI_UNSUPPORTED) the last known-good cached value is retained rather than
overwritten, so a transient firmware error does not zero out the
reported capacity.
Measured on an Intel Xeon E5-2628L v4 (6.12 kernel), per statfs(2) on the
efivarfs mount:
# fs.efivarfs.statfs_refresh = 1 (firmware call on the hot path)
$ strace -T -e trace=statfs df 2>&1 | grep -i efivars
statfs("/sys/firmware/efi/efivars", {...}) = 0 <0.062993>
statfs("/sys/firmware/efi/efivars", {...}) = 0 <0.064343>
# after: sysctl fs.efivarfs.statfs_refresh=0 (served from cache)
$ strace -T -e trace=statfs df 2>&1 | grep -i efivars
statfs("/sys/firmware/efi/efivars", {...}) = 0 <0.000009>
# in a tight df loop with refresh=1, rate-limited hits show the cache
# (sub-ms) interleaved with the ~63ms firmware calls:
statfs(...) = 0 <0.063705>
statfs(...) = 0 <0.000429>
statfs(...) = 0 <0.000079>
force_refresh picks up an intervening variable write on demand without
re-enabling the hot-path call:
$ df -h | grep efivars
efivarfs 88K 54K 30K 65% /sys/firmware/efi/efivars
$ sysctl -w fs.efivarfs.force_refresh=1
$ df -h | grep efivars
efivarfs 88K 55K 29K 66% /sys/firmware/efi/efivars
Signed-off-by: Prashant Singh <singhpra@juniper.net>
---
An alternative would be to expose statfs_refresh as an efivarfs mount
option instead of a global sysctl (per-mount, no new sysctl ABI, and a
remount could re-prime the cache in place of force_refresh). I went with
the sysctl for a live runtime toggle and because the variable store /
cache are inherently global, but I'm happy to respin as a mount option
if that's preferred.
Documentation/admin-guide/sysctl/fs.rst | 24 +++++
fs/efivarfs/super.c | 132 +++++++++++++++++++-----
2 files changed, 128 insertions(+), 28 deletions(-)
diff --git a/Documentation/admin-guide/sysctl/fs.rst b/Documentation/admin-guide/sysctl/fs.rst
index 9b7f65c3efd8..4b6683b6791c 100644
--- a/Documentation/admin-guide/sysctl/fs.rst
+++ b/Documentation/admin-guide/sysctl/fs.rst
@@ -76,6 +76,30 @@ they help speeding up rejection of non-existing files provided
by the users.
+efivarfs
+--------
+
+These entries appear under ``/proc/sys/fs/efivarfs`` and control how
+``statfs(2)`` on an efivarfs mount reports the EFI variable-store
+capacity. The capacity is obtained from the ``QueryVariableInfo()`` EFI
+runtime service, which on some x86 platforms can take tens of
+milliseconds. To avoid this cost on the ``statfs(2)`` hot path the
+reported capacity is cached; these knobs control how the cache is
+refreshed.
+
+``statfs_refresh`` (default 1)
+ When set to 1, ``statfs(2)`` refreshes the cached capacity by calling
+ the firmware, rate-limited to a few calls every couple of seconds.
+ When set to 0, ``statfs(2)`` never calls firmware and always serves the
+ cached value; the cache is then updated only at mount time and by
+ ``force_refresh``. Set this to 0 on latency-sensitive systems.
+
+``force_refresh`` (write-only)
+ Writing any value forces an immediate, synchronous re-query of the
+ firmware to update the cache. Useful to pick up capacity changes when
+ ``statfs_refresh`` is 0. The written value is ignored.
+
+
file-max & file-nr
------------------
diff --git a/fs/efivarfs/super.c b/fs/efivarfs/super.c
index 8d33f11db2a1..1e7e81766c28 100644
--- a/fs/efivarfs/super.c
+++ b/fs/efivarfs/super.c
@@ -19,6 +19,7 @@
#include <linux/notifier.h>
#include <linux/printk.h>
#include <linux/namei.h>
+#include <linux/sysctl.h>
#include "internal.h"
#include "../internal.h"
@@ -77,44 +78,71 @@ static int efivarfs_show_options(struct seq_file *m, struct dentry *root)
return 0;
}
-static int efivarfs_statfs(struct dentry *dentry, struct kstatfs *buf)
+/*
+ * Cached EFI variable-store capacity: QueryVariableInfo() is costly on some
+ * firmware, so statfs() serves this cache instead of calling firmware.
+ */
+static DEFINE_SPINLOCK(efivarfs_cap_lock);
+static u64 efivarfs_cap_storage;
+static u64 efivarfs_cap_remaining;
+
+/*
+ * fs.efivarfs.statfs_refresh (default 1): when set, statfs() refreshes the
+ * cached capacity, rate-limited. Set to 0 on latency-sensitive systems so
+ * statfs() never calls firmware; the cache is then updated only at mount and by
+ * fs.efivarfs.force_refresh.
+ */
+static int efivarfs_statfs_refresh = 1;
+
+static void efivarfs_query_capacity(void)
{
const u32 attr = EFI_VARIABLE_NON_VOLATILE |
EFI_VARIABLE_BOOTSERVICE_ACCESS |
EFI_VARIABLE_RUNTIME_ACCESS;
- u64 storage_space, remaining_space, max_variable_size;
- u64 id = huge_encode_dev(dentry->d_sb->s_dev);
+ u64 storage_space = 0, remaining_space = 0, max_variable_size;
efi_status_t status;
- /* Some UEFI firmware does not implement QueryVariableInfo() */
- storage_space = remaining_space = 0;
- if (efi_rt_services_supported(EFI_RT_SUPPORTED_QUERY_VARIABLE_INFO)) {
+ if (!efivar_is_available() ||
+ !efi_rt_services_supported(EFI_RT_SUPPORTED_QUERY_VARIABLE_INFO))
+ return;
+
+ status = efivar_query_variable_info(attr, &storage_space,
+ &remaining_space, &max_variable_size);
+ if (status != EFI_SUCCESS && status != EFI_UNSUPPORTED) {
+ pr_warn_ratelimited("query_variable_info() failed: 0x%lx\n", status);
+ return;
+ }
+
+ spin_lock(&efivarfs_cap_lock);
+ efivarfs_cap_storage = storage_space;
+ efivarfs_cap_remaining = remaining_space;
+ spin_unlock(&efivarfs_cap_lock);
+}
+
+static int efivarfs_statfs(struct dentry *dentry, struct kstatfs *buf)
+{
+ u64 id = huge_encode_dev(dentry->d_sb->s_dev);
+ u64 storage_space, remaining_space;
+
+ /*
+ * Refresh the cached capacity (rate-limited) unless statfs_refresh is
+ * disabled, in which case statfs() serves the cache without ever calling
+ * the expensive QueryVariableInfo() firmware service.
+ */
+ if (READ_ONCE(efivarfs_statfs_refresh)) {
static DEFINE_RATELIMIT_STATE(_rs, 2 * HZ, 5);
- static u64 storage, remaining;
- static DEFINE_SPINLOCK(lock);
- if (!__ratelimit(&_rs)) {
+ if (__ratelimit(&_rs))
+ efivarfs_query_capacity();
+ else
ratelimit_set_flags(&_rs, RATELIMIT_MSG_ON_RELEASE);
-
- spin_lock(&lock);
- storage_space = storage;
- remaining_space = remaining;
- spin_unlock(&lock);
- } else {
- status = efivar_query_variable_info(attr, &storage_space,
- &remaining_space,
- &max_variable_size);
- if (status != EFI_SUCCESS && status != EFI_UNSUPPORTED)
- pr_warn("query_variable_info() failed: 0x%lx\n",
- status);
-
- spin_lock(&lock);
- storage = storage_space;
- remaining = remaining_space;
- spin_unlock(&lock);
- }
}
+ spin_lock(&efivarfs_cap_lock);
+ storage_space = efivarfs_cap_storage;
+ remaining_space = efivarfs_cap_remaining;
+ spin_unlock(&efivarfs_cap_lock);
+
/*
* This is not a normal filesystem, so no point in pretending it has a block
* size; we declare f_bsize to 1, so that we can then report the exact value
@@ -392,6 +420,9 @@ static int efivarfs_fill_super(struct super_block *sb, struct fs_context *fc)
if (err)
return err;
+ /* Prime the capacity cache once at mount so df has valid numbers. */
+ efivarfs_query_capacity();
+
return efivar_init(efivarfs_callback, sb, true);
}
@@ -552,13 +583,58 @@ static struct file_system_type efivarfs_type = {
.fs_flags = FS_POWER_FREEZE,
};
+static int efivarfs_force_refresh;
+
+static int efivarfs_force_refresh_handler(const struct ctl_table *table, int write,
+ void *buffer, size_t *lenp, loff_t *ppos)
+{
+ int ret = proc_dointvec(table, write, buffer, lenp, ppos);
+
+ /* The written value is irrelevant; any write triggers a re-query. */
+ if (!ret && write)
+ efivarfs_query_capacity();
+ return ret;
+}
+
+static const struct ctl_table efivarfs_sysctls[] = {
+ {
+ .procname = "statfs_refresh",
+ .data = &efivarfs_statfs_refresh,
+ .maxlen = sizeof(int),
+ .mode = 0644,
+ .proc_handler = proc_dointvec_minmax,
+ .extra1 = SYSCTL_ZERO,
+ .extra2 = SYSCTL_ONE,
+ },
+ {
+ .procname = "force_refresh",
+ .data = &efivarfs_force_refresh,
+ .maxlen = sizeof(int),
+ .mode = 0200,
+ .proc_handler = efivarfs_force_refresh_handler,
+ },
+};
+
+static struct ctl_table_header *efivarfs_sysctl_header;
+
static __init int efivarfs_init(void)
{
- return register_filesystem(&efivarfs_type);
+ int err = register_filesystem(&efivarfs_type);
+
+ if (err)
+ return err;
+
+ efivarfs_sysctl_header = register_sysctl("fs/efivarfs", efivarfs_sysctls);
+ if (!efivarfs_sysctl_header)
+ pr_warn("efivarfs: unable to register sysctls\n");
+
+ return 0;
}
static __exit void efivarfs_exit(void)
{
+ if (efivarfs_sysctl_header)
+ unregister_sysctl_table(efivarfs_sysctl_header);
unregister_filesystem(&efivarfs_type);
}
--
2.34.1
base-commit: 9b87fdc9af2fbfcdb5c24a64139685ef80f6573f
^ permalink raw reply [flat|nested] 3+ messages in thread* Re: [PATCH] efivarfs: avoid slow QueryVariableInfo() in statfs()
2026-09-17 7:16 [PATCH] efivarfs: avoid slow QueryVariableInfo() in statfs() Prashant Singh
@ 2026-09-17 8:36 ` Ard Biesheuvel
2026-09-17 16:50 ` Prashant Singh
0 siblings, 1 reply; 3+ messages in thread
From: Ard Biesheuvel @ 2026-09-17 8:36 UTC (permalink / raw)
To: Prashant Singh, Jeremy Kerr
Cc: linux-efi, linux-kernel, Jonathan Corbet, linux-doc
Hello Prashant,
On Thu, 17 Sep 2026, at 09:16, Prashant Singh wrote:
> QueryVariableInfo() is an EFI runtime service that, on some platforms,
> takes tens of milliseconds and runs with preemption disabled, freezing
> the CPU that services it for the whole call. efivarfs_statfs() issued
> this call on every statfs(2) (e.g. every "df"), which produced large
> latency spikes for unrelated latency-sensitive workloads pinned to the
> same CPU.
>
> Commit b2326338dc68 ("efivarfs: Rate limit statfs() handler") already
> bounds the firmware call to twice per second. That helps against a
> statfs() flood, but on a real-time / dataplane system even the residual
> 2 calls/s x ~40ms = up to ~80ms/s of preempt-disabled CPU time is
> unacceptable: any unprivileged process (or a periodic monitoring "df")
> can still inject ~40ms stalls into a co-located latency-sensitive task.
>
Could we instead introduce a mount option that disables used/available
reporting entirely? Feel free to set the default to 'disabled' when
CONFIG_PREEMPT_RT is set.
^ permalink raw reply [flat|nested] 3+ messages in thread* Re: [PATCH] efivarfs: avoid slow QueryVariableInfo() in statfs()
2026-09-17 8:36 ` Ard Biesheuvel
@ 2026-09-17 16:50 ` Prashant Singh
0 siblings, 0 replies; 3+ messages in thread
From: Prashant Singh @ 2026-09-17 16:50 UTC (permalink / raw)
To: Ard Biesheuvel, Jeremy Kerr
Cc: linux-efi, linux-kernel, Jonathan Corbet, linux-doc
On Thu, 17 Sep 2026, at 10:36, Ard Biesheuvel wrote:
> Could we instead introduce a mount option that disables used/available
> reporting entirely? Feel free to set the default to 'disabled' when
> CONFIG_PREEMPT_RT is set.
Will do. I'll add a bool mount option (e.g. statfs_capacity) that, when
disabled, skips QueryVariableInfo() in statfs() and reports 0
used/available, defaulting to disabled under CONFIG_PREEMPT_RT.
If you'd rather df still show numbers without the per-statfs firmware
call, I could instead prime the value once at mount and serve that
(stale but non-zero) as a "cached" mode -- i.e. a tri-state option:
statfs_capacity=live|cached|off
live - current behaviour: rate-limited QueryVariableInfo() on statfs()
cached - prime once at mount, serve the static value (no per-statfs call)
off - report 0 used/available (no firmware call at all)
default: live on non-RT, off on CONFIG_PREEMPT_RT
Happy either way.
Thanks,
Prashant
^ permalink raw reply [flat|nested] 3+ messages in thread
end of thread, other threads:[~2026-09-17 16:51 UTC | newest]
Thread overview: 3+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2026-09-17 7:16 [PATCH] efivarfs: avoid slow QueryVariableInfo() in statfs() Prashant Singh
2026-09-17 8:36 ` Ard Biesheuvel
2026-09-17 16:50 ` Prashant Singh
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox
all inboxes | Powered by JetHome®