lists.openwall.net   lists  /  announce  owl-users  owl-dev  john-users  john-dev  passwdqc-users  yescrypt  popa3d-users  /  oss-security  kernel-hardening  musl  sabotage  tlsify  passwords  /  crypt-dev  xvendor  /  Bugtraq  Full-Disclosure  linux-kernel  linux-netdev  linux-ext4  linux-hardening  linux-cve-announce  PHC 
Open Source and information security mailing list archives
 
Hash Suite for Android: free password hash cracker in your pocket
[<prev] [next>] [<thread-prev] [thread-next>] [day] [month] [year] [list]
Message-Id: <907e4f14cedb2bfb2810dec255b930d59b821dc2.1768324215.git.wen.yang@linux.dev>
Date: Wed, 14 Jan 2026 01:40:32 +0800
From: wen.yang@...ux.dev
To: Joel Granados <joel.granados@...nel.org>
Cc: linux-kernel@...r.kernel.org,
	Wen Yang <wen.yang@...ux.dev>
Subject: [RFC PATCH 3/4] sysctl: support encoding values directly in the table entry

From: Wen Yang <wen.yang@...ux.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@...nel.org>
Signed-off-by: Wen Yang <wen.yang@...ux.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


Powered by blists - more mailing lists

Powered by Openwall GNU/*/Linux Powered by OpenVZ