mirror of https://lore.kernel.org/lkml/
 help / color / mirror / Atom feed
From: wen.yang@linux.dev
To: Joel Granados <joel.granados@kernel.org>
Cc: linux-kernel@vger.kernel.org, Wen Yang <wen.yang@linux.dev>
Subject: [RFC PATCH 3/4] sysctl: support encoding values directly in the table entry
Date: Wed, 14 Jan 2026 01:40:32 +0800	[thread overview]
Message-ID: <907e4f14cedb2bfb2810dec255b930d59b821dc2.1768324215.git.wen.yang@linux.dev> (raw)
In-Reply-To: <cover.1768324215.git.wen.yang@linux.dev>

From: Wen Yang <wen.yang@linux.dev>

Eric points out: "by turning .extra1 and .extra2 into longs instead of
keeping them as pointers and needing constants to be pointed at somewhere
.. The only people I can see who find a significant benefit by
consolidating all of the constants into one place are people who know how
to stomp kernel memory."

This patch supports encoding values directly in table entries.
This patch also adds a kunit test case:
[15:09:24] ================ sysctl_test (11 subtests) =================
[15:09:24] [PASSED] sysctl_test_api_dointvec_null_tbl_data
[15:09:24] [PASSED] sysctl_test_api_dointvec_table_maxlen_unset
[15:09:24] [PASSED] sysctl_test_api_dointvec_table_len_is_zero
[15:09:24] [PASSED] sysctl_test_api_dointvec_table_read_but_position_set
[15:09:24] [PASSED] sysctl_test_dointvec_read_happy_single_positive
[15:09:24] [PASSED] sysctl_test_dointvec_read_happy_single_negative
[15:09:24] [PASSED] sysctl_test_dointvec_write_happy_single_positive
[15:09:24] [PASSED] sysctl_test_dointvec_write_happy_single_negative
[15:09:24] [PASSED] sysctl_test_api_dointvec_write_single_less_int_min
[15:09:24] [PASSED] sysctl_test_api_dointvec_write_single_greater_int_max
[15:09:24] [PASSED] sysctl_test_api_dointvec_write_range_check
[15:09:24] =================== [PASSED] sysctl_test ===================
[15:09:24] ============================================================
[15:09:24] Testing complete. Ran 11 tests: passed: 11

Suggested-by: Joel Granados <joel.granados@kernel.org>
Signed-off-by: Wen Yang <wen.yang@linux.dev>
---
 include/linux/sysctl.h |  60 +++++++++++++++--
 kernel/sysctl-test.c   | 142 +++++++++++++++++++++++++++++++++++++++++
 2 files changed, 195 insertions(+), 7 deletions(-)

diff --git a/include/linux/sysctl.h b/include/linux/sysctl.h
index 9690740885ab..19be4f3f700b 100644
--- a/include/linux/sysctl.h
+++ b/include/linux/sysctl.h
@@ -165,9 +165,14 @@ static inline void *proc_sys_poll_event(struct ctl_table_poll *poll)
 	struct ctl_table_poll name = __CTL_TABLE_POLL_INITIALIZER(name)
 
 #define SYSCTL_IN_RANGE(tbl, val, type) \
-	((tbl) && \
-	 (!(tbl)->extra1 || (*(type *)(tbl)->extra1 <= (type)(val))) && \
-	 (!(tbl)->extra2 || (*(type *)(tbl)->extra2 >= (type)(val))))
+	((tbl) && ({ \
+		type v = (type)(val); \
+		!((tbl)->min_is_value ? (tbl)->min > (unsigned long)v \
+				: (tbl)->extra1 && *(type*)(tbl)->extra1 > v) \
+		&& !((tbl)->max_is_value ? (tbl)->max < (unsigned long)v \
+				: (tbl)->extra2 && *(type*)(tbl)->extra2 < v); \
+		}) \
+	)
 
 #define SYSCTL_IN_RANGE_INT(tbl, val)           SYSCTL_IN_RANGE(tbl, val, int)
 #define SYSCTL_IN_RANGE_LONG(tbl, val)          SYSCTL_IN_RANGE(tbl, val, long)
@@ -180,12 +185,52 @@ struct ctl_table {
 	void *data;
 	int maxlen;
 	umode_t mode;
+
+	/**
+	 * Range bound type flags
+	 * - min_is_value: 1 = min field is direct value, 0 = extra1 is pointer
+	 * - max_is_value: 1 = max field is direct value, 0 = extra2 is pointer
+	 */
+	union {
+		struct {
+			unsigned char min_is_value : 1;  /* bit 0: min type */
+			unsigned char max_is_value : 1;  /* bit 1: max type */
+			unsigned char reserved     : 6;  /* bits 2-7: reserved */
+		};
+		unsigned char flags;
+	};
+
 	proc_handler *proc_handler;	/* Callback for text formatting */
 	struct ctl_table_poll *poll;
-	void *extra1;
-	void *extra2;
+
+	/* Range bounds: either pointers or direct values */
+	union {
+		struct {
+			void *extra1;          /* min as pointer */
+			void *extra2;          /* max as pointer */
+		};
+		struct {
+			unsigned long min;     /* min as direct value */
+			unsigned long max;     /* max as direct value */
+		};
+	};
 } __randomize_layout;
 
+/* Pointer detection using _Generic */
+#define IS_PTR(x) _Generic((x), \
+        char*:1, short*:1, int*:1, long*:1, \
+        unsigned char*:1, unsigned short*:1, unsigned int*:1, unsigned long*:1, \
+        const char*:1, const int*:1, const long*:1, \
+        void*:1, const void*:1, default:0)
+
+/* Auto-detect range flags: 0=ptr_ptr, 1=val_ptr, 2=ptr_val, 3=val_val */
+#define RANGE_FLAGS(smin, smax)  ((!IS_PTR(smin)) | ((!IS_PTR(smax)) << 1))
+
+/* Type-specific checks */
+#define CHECK_INT(tbl, val)     CHECK(tbl, val, int)
+#define CHECK_UINT(tbl, val)    CHECK(tbl, val, unsigned int)
+#define CHECK_LONG(tbl, val)    CHECK(tbl, val, long)
+
 #define __SYSCTL_ENTRY(NAME, DATA, TYPE, MODE, HANDLER, SMIN, SMAX)\
 	{							\
 		.procname	= NAME,				\
@@ -193,8 +238,9 @@ struct ctl_table {
 		.maxlen		= sizeof(TYPE),			\
 		.mode		= MODE,				\
 		.proc_handler	= HANDLER,			\
-		.extra1		= SMIN,				\
-		.extra2		= SMAX,				\
+		.flags		= RANGE_FLAGS(SMIN, SMAX),      \
+		.min		= (unsigned long)(SMIN),        \
+		.max		= (unsigned long)(SMAX),        \
 	}
 
 #define SYSCTL_ENTRY(NAME, DATA, TYPE, MODE)			\
diff --git a/kernel/sysctl-test.c b/kernel/sysctl-test.c
index 7fb0e7f1e62f..b55bc84e56c2 100644
--- a/kernel/sysctl-test.c
+++ b/kernel/sysctl-test.c
@@ -300,6 +300,147 @@ static void sysctl_test_api_dointvec_write_single_greater_int_max(
 	KUNIT_EXPECT_EQ(test, 0, *((int *)table.data));
 }
 
+/*
+ * Test that writing values within the specified range succeeds,
+ * and ensures out-of-range writes are correctly rejected.
+ */
+static void sysctl_test_api_dointvec_write_range_check(struct kunit *test)
+{
+	int data = 0;
+	size_t len;
+	loff_t pos;
+	char *buffer;
+	char __user *user_buffer;
+
+	/* Prepare bound variables for pointer modes */
+	int min_bounds[] = {10, 0, 15, 0};
+	int max_bounds[] = {90, 75, 0, 0};
+
+	/* Initialize test tables using macro */
+	struct ctl_table test_cases[] = {
+		/* PTR_PTR: both pointers */
+		SYSCTL_RANGE_ENTRY("ptr_ptr", &data, int, 0644,
+				&min_bounds[0], &max_bounds[0]),
+		/* VAL_PTR: min is value, max is pointer */
+		SYSCTL_RANGE_ENTRY("val_ptr", &data, int, 0644,
+				25, &max_bounds[1]),
+		/* PTR_VAL: min is pointer, max is value */
+		SYSCTL_RANGE_ENTRY("ptr_val", &data, int, 0644,
+				&min_bounds[2], 85),
+		/* VAL_VAL: both values */
+		SYSCTL_RANGE_ENTRY("val_val", &data, int, 0644,
+				20, 80),
+	};
+
+	/* Test parameters for each mode */
+	const struct {
+		int min, max;
+		const char *valid;
+		const char *below_min;
+		const char *above_max;
+	} test_params[] = {
+		{10, 90, "50", "5",  "95"},  /* PTR_PTR */
+		{25, 75, "50", "20", "80"},  /* VAL_PTR */
+		{15, 85, "50", "10", "90"},  /* PTR_VAL */
+		{20, 80, "50", "15", "85"},  /* VAL_VAL */
+	};
+
+	for (int i = 0; i < 4; i++) {
+		struct ctl_table *table = &test_cases[i];
+		const typeof(test_params[0]) *param = &test_params[i];
+
+		/* Verify flags auto-detection */
+		KUNIT_EXPECT_EQ(test, table->flags, i);
+		KUNIT_EXPECT_EQ(test, (int)table->min_is_value, (i & 1) != 0);
+		KUNIT_EXPECT_EQ(test, (int)table->max_is_value, (i & 2) != 0);
+
+		/* Verify union access */
+		if (table->min_is_value) {
+			KUNIT_EXPECT_EQ(test, table->min, (unsigned long)param->min);
+		} else {
+			KUNIT_EXPECT_PTR_EQ(test, table->extra1, &min_bounds[i]);
+			KUNIT_EXPECT_EQ(test, *(int *)table->extra1, param->min);
+		}
+
+		if (table->max_is_value) {
+			KUNIT_EXPECT_EQ(test, table->max, (unsigned long)param->max);
+		} else {
+			KUNIT_EXPECT_PTR_EQ(test, table->extra2, &max_bounds[i]);
+			KUNIT_EXPECT_EQ(test, *(int *)table->extra2, param->max);
+		}
+
+		/* Test valid value in range */
+		data = 0;
+		len = strlen(param->valid);
+		pos = 0;
+		buffer = kunit_kzalloc(test, len, GFP_USER);
+		user_buffer = (char __user *)buffer;
+		memcpy(buffer, param->valid, len);
+
+		KUNIT_EXPECT_EQ(test, 0, proc_dointvec_minmax(table, KUNIT_PROC_WRITE,
+					user_buffer, &len, &pos));
+		KUNIT_EXPECT_EQ(test, strlen(param->valid), len);
+		KUNIT_EXPECT_EQ(test, strlen(param->valid), pos);
+		KUNIT_EXPECT_EQ(test, 50, data);
+
+		/* Test min boundary */
+		data = 0;
+		char min_str[16];
+		snprintf(min_str, sizeof(min_str), "%d", param->min);
+		len = strlen(min_str);
+		pos = 0;
+		buffer = kunit_kzalloc(test, len, GFP_USER);
+		user_buffer = (char __user *)buffer;
+		memcpy(buffer, min_str, len);
+
+		KUNIT_EXPECT_EQ(test, 0, proc_dointvec_minmax(table, KUNIT_PROC_WRITE,
+					user_buffer, &len, &pos));
+		KUNIT_EXPECT_EQ(test, strlen(min_str), len);
+		KUNIT_EXPECT_EQ(test, strlen(min_str), pos);
+		KUNIT_EXPECT_EQ(test, param->min, data);
+
+		/* Test max boundary */
+		data = 0;
+		char max_str[16];
+		snprintf(max_str, sizeof(max_str), "%d", param->max);
+		len = strlen(max_str);
+		pos = 0;
+		buffer = kunit_kzalloc(test, len, GFP_USER);
+		user_buffer = (char __user *)buffer;
+		memcpy(buffer, max_str, len);
+
+		KUNIT_EXPECT_EQ(test, 0, proc_dointvec_minmax(table, KUNIT_PROC_WRITE,
+					user_buffer, &len, &pos));
+		KUNIT_EXPECT_EQ(test, strlen(max_str), len);
+		KUNIT_EXPECT_EQ(test, strlen(max_str), pos);
+		KUNIT_EXPECT_EQ(test, param->max, data);
+
+		/* Test below min - should fail */
+		data = 0;
+		len = strlen(param->below_min);
+		pos = 0;
+		buffer = kunit_kzalloc(test, len, GFP_USER);
+		user_buffer = (char __user *)buffer;
+		memcpy(buffer, param->below_min, len);
+
+		KUNIT_EXPECT_EQ(test, -EINVAL, proc_dointvec_minmax(table, KUNIT_PROC_WRITE,
+					user_buffer, &len, &pos));
+		KUNIT_EXPECT_EQ(test, 0, data);
+
+		/* Test above max - should fail */
+		data = 0;
+		len = strlen(param->above_max);
+		pos = 0;
+		buffer = kunit_kzalloc(test, len, GFP_USER);
+		user_buffer = (char __user *)buffer;
+		memcpy(buffer, param->above_max, len);
+
+		KUNIT_EXPECT_EQ(test, -EINVAL, proc_dointvec_minmax(table, KUNIT_PROC_WRITE,
+					user_buffer, &len, &pos));
+		KUNIT_EXPECT_EQ(test, 0, data);
+	}
+}
+
 static struct kunit_case sysctl_test_cases[] = {
 	KUNIT_CASE(sysctl_test_api_dointvec_null_tbl_data),
 	KUNIT_CASE(sysctl_test_api_dointvec_table_maxlen_unset),
@@ -311,6 +452,7 @@ static struct kunit_case sysctl_test_cases[] = {
 	KUNIT_CASE(sysctl_test_dointvec_write_happy_single_negative),
 	KUNIT_CASE(sysctl_test_api_dointvec_write_single_less_int_min),
 	KUNIT_CASE(sysctl_test_api_dointvec_write_single_greater_int_max),
+	KUNIT_CASE(sysctl_test_api_dointvec_write_range_check),
 	{}
 };
 
-- 
2.25.1


  parent reply	other threads:[~2026-01-13 17:41 UTC|newest]

Thread overview: 8+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-01-13 17:40 [RFC PATCH 0/4] sysctl: refactor ctl_table creation and change extra{1,2} type wen.yang
2026-01-13 17:40 ` [RFC PATCH 1/4] sysctl: add SYSCTL_ENTRY/SYSCTL_RANGE_ENTRY helpers for ctl_table init wen.yang
2026-01-21 14:30   ` Joel Granados
2026-01-22  9:56   ` Joel Granados
2026-01-25 20:08     ` Wen Yang
2026-01-13 17:40 ` [RFC PATCH 2/4] sysctl: add helper functions to extract table->extra1/extra2 wen.yang
2026-01-13 17:40 ` wen.yang [this message]
2026-01-13 17:40 ` [RFC PATCH 4/4] scripts/coccinelle: add sysctl_table_init.cocci wen.yang

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=907e4f14cedb2bfb2810dec255b930d59b821dc2.1768324215.git.wen.yang@linux.dev \
    --to=wen.yang@linux.dev \
    --cc=joel.granados@kernel.org \
    --cc=linux-kernel@vger.kernel.org \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
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®