[PATCH v2] sched/debug: Validate writes to the scan_size_mb debugfs knob

From: Zhan Xusheng

Date: Mon Aug 10 2026 - 10:27:02 EST


scan_size_mb ends up as a divisor and debugfs_create_u32() stores whatever
is written, so two values panic the kernel.

Zero, through MAX_SCAN_WINDOW / scan_size in task_scan_min():

# echo 0 > /sys/kernel/debug/sched/numa_balancing/scan_size_mb

Oops: divide error: 0000 [#1] SMP PTI
RIP: 0010:task_scan_max+0x30/0x1a0
RAX: 0000000000000a00 RCX: 0000000000000000
Call Trace:
init_numa_balancing+0xe0/0x200
__sched_fork+0x13b/0x180
sched_fork+0x12/0x1d0
copy_process+0xdea/0x2370
kernel_clone+0xd6/0x4a0

task_scan_min() is inlined into task_scan_max() there. It takes a CLONE_VM
child rather than a fork, since init_numa_balancing() returns before that
call for a new address space.

And a multiple of 2^24 with 4K pages: MB_TO_PAGES() shifts an unsigned int,
so it wraps to zero and task_nr_scan_windows() divides by that.

# echo 16777216 > /sys/kernel/debug/sched/numa_balancing/scan_size_mb
RIP: 0010:task_nr_scan_windows.isra.0+0x5c/0x70

The sysctl this knob replaced enforced the lower bound with
.extra1 = SYSCTL_ONE. The move to debugfs kept validation for
tunable_scaling, which got sched_scaling_fops, but scan_size_mb became a
plain u32 and lost it. The upper bound was never enforced.

Reject both the way sched_scaling_fops does. scan_size_mb is documented in
Documentation/scheduler/sched-debug.rst, so -EINVAL is better than storing
a value that panics on the next clone. Hex input keeps working, as
simple_attr_write_xsigned() allowed.

Verified in a 2-node qemu guest: both writes panic v7.2-rc6 at the RIPs
above; with this patch 0, 2^24 and 3*2^24 get -EINVAL and 512 still takes
effect.

Fixes: 8a99b6833c88 ("sched: Move SCHED_DEBUG sysctl to debugfs")
Signed-off-by: Zhan Xusheng <zhanxusheng@xxxxxxxxxx>
Reviewed-by: Chen Yu <yu.c.chen@xxxxxxxxx>
---
v2:
- parse with base 0 so hex writes keep working, matching the
kstrtoull(attr->set_buf, 0, &val) in simple_attr_write_xsigned() that
debugfs_create_u32() used (Chen Yu)
- name that helper precisely in the changelog
- Link to v1: https://lore.kernel.org/lkml/20260810081829.3149958-1-zhanxusheng@xxxxxxxxxx/
kernel/sched/debug.c | 51 +++++++++++++++++++++++++++++++++++++++++++-
1 file changed, 50 insertions(+), 1 deletion(-)

diff --git a/kernel/sched/debug.c b/kernel/sched/debug.c
index 40584b27ea0c..01e74671e439 100644
--- a/kernel/sched/debug.c
+++ b/kernel/sched/debug.c
@@ -210,6 +210,55 @@ static const struct file_operations sched_scaling_fops = {
.release = single_release,
};

+#ifdef CONFIG_NUMA_BALANCING
+/*
+ * task_scan_min() divides MAX_SCAN_WINDOW by this, and
+ * task_nr_scan_windows() divides by MB_TO_PAGES() of it. Zero breaks the
+ * first; a value that overflows the 32-bit shift in MB_TO_PAGES() wraps to
+ * zero and breaks the second.
+ */
+#define NUMA_SCAN_SIZE_MB_MAX (UINT_MAX >> (20 - PAGE_SHIFT))
+
+static ssize_t sched_numa_scan_size_write(struct file *filp,
+ const char __user *ubuf,
+ size_t cnt, loff_t *ppos)
+{
+ unsigned int mb;
+ int ret;
+
+ ret = kstrtouint_from_user(ubuf, cnt, 0, &mb);
+ if (ret)
+ return ret;
+
+ if (!mb || mb > NUMA_SCAN_SIZE_MB_MAX)
+ return -EINVAL;
+
+ sysctl_numa_balancing_scan_size = mb;
+
+ *ppos += cnt;
+ return cnt;
+}
+
+static int sched_numa_scan_size_show(struct seq_file *m, void *v)
+{
+ seq_printf(m, "%u\n", sysctl_numa_balancing_scan_size);
+ return 0;
+}
+
+static int sched_numa_scan_size_open(struct inode *inode, struct file *filp)
+{
+ return single_open(filp, sched_numa_scan_size_show, NULL);
+}
+
+static const struct file_operations sched_numa_scan_size_fops = {
+ .open = sched_numa_scan_size_open,
+ .write = sched_numa_scan_size_write,
+ .read = seq_read,
+ .llseek = seq_lseek,
+ .release = single_release,
+};
+#endif /* CONFIG_NUMA_BALANCING */
+
#ifdef CONFIG_SCHED_CACHE
static ssize_t
sched_cache_enable_write(struct file *filp, const char __user *ubuf,
@@ -664,7 +713,7 @@ static __init int sched_init_debug(void)
debugfs_create_u32("scan_delay_ms", 0644, numa, &sysctl_numa_balancing_scan_delay);
debugfs_create_u32("scan_period_min_ms", 0644, numa, &sysctl_numa_balancing_scan_period_min);
debugfs_create_u32("scan_period_max_ms", 0644, numa, &sysctl_numa_balancing_scan_period_max);
- debugfs_create_u32("scan_size_mb", 0644, numa, &sysctl_numa_balancing_scan_size);
+ debugfs_create_file("scan_size_mb", 0644, numa, NULL, &sched_numa_scan_size_fops);
debugfs_create_u32("hot_threshold_ms", 0644, numa, &sysctl_numa_balancing_hot_threshold);
#endif /* CONFIG_NUMA_BALANCING */

--
2.43.0