From: wen.yang@linux.dev
To: Joel Granados <joel.granados@kernel.org>
Cc: Wen Yang <wen.yang@linux.dev>,
linux-fsdevel@vger.kernel.org, linux-kernel@vger.kernel.org
Subject: [RFC PATCH 03/39] sysctl: add KUnit coverage for numeric bounds and handlers
Date: Thu, 29 Oct 2026 00:34:42 +0800 [thread overview]
Message-ID: <3f6be87a6685418f170b6713c1759a9c16573871.1793204511.git.wen.yang@linux.dev> (raw)
In-Reply-To: <cover.1793204511.git.wen.yang@linux.dev>
From: Wen Yang <wen.yang@linux.dev>
Give numeric bounds and the handlers that consume them executable
coverage: full type ranges, one-sided and zero bounds, entries without
backing storage, explicit handlers for arrays and members, and rejection
of writes that would partially commit.
The test file uses a small set of local constructor macros so the cases
stay readable. They are private to the test, not a kernel API, and have
no production users.
Suggested-by: Joel Granados <joel.granados@kernel.org>
Signed-off-by: Wen Yang <wen.yang@linux.dev>
---
kernel/sysctl-test.c | 634 ++++++++++++++++++++++++++++++++++---------
1 file changed, 504 insertions(+), 130 deletions(-)
diff --git a/kernel/sysctl-test.c b/kernel/sysctl-test.c
index 92f94ea28957..f18ec901443f 100644
--- a/kernel/sysctl-test.c
+++ b/kernel/sysctl-test.c
@@ -4,39 +4,146 @@
*/
#include <kunit/test.h>
+#include <linux/jiffies.h>
+#include <linux/limits.h>
#include <linux/sysctl.h>
+#if IS_ENABLED(CONFIG_NET)
+#include <net/net_namespace.h>
+#endif
+
+/*
+ * Constructors for test entries. They are local to this file: they are not
+ * a kernel API and have no production users.
+ */
+
+/*
+ * Local constructors for test entries: variable-first, with the suffix
+ * letters spelling out the arguments. They are not a kernel API.
+ */#define __CTLTBL_ENTRY_INIT(DATA, NAME, MODE, HANDLER, MIN, MAX, MAXLEN) \
+ { \
+ .procname = "" NAME "", \
+ .data = (DATA), \
+ .maxlen = (MAXLEN), \
+ .mode = (MODE), \
+ .proc_handler = (HANDLER), \
+ .poll = NULL, \
+ .min = (MIN), \
+ .max = (MAX), \
+ .priv = NULL, \
+ }
+
+#define __CTLTBL_ENTRY(VAR, NAME, MODE, HANDLER, MIN, MAX, MAXLEN) \
+ __CTLTBL_ENTRY_INIT(&(VAR), NAME, MODE, HANDLER, MIN, MAX, MAXLEN)
+
+/* No default association: unsupported automatic types fail to compile. */
+#define __CTLTBL_AUTO_HANDLER(VAR) \
+ _Generic((VAR), \
+ signed int: proc_dointvec, \
+ unsigned int: proc_douintvec, \
+ unsigned long: proc_doulongvec_minmax)
+
+#define __CTLTBL_AUTO_HANDLER_MINMAX(VAR) \
+ _Generic((VAR), \
+ signed int: proc_dointvec_minmax, \
+ unsigned int: proc_douintvec_minmax, \
+ unsigned long: proc_doulongvec_minmax)
+
+/* Bounds do not evaluate VAR, including in explicit-handler forms. */
+#define __CTLTBL_MIN(VAR) \
+ _Generic((VAR), signed int: INT_MIN, default : 0)
+
+#define __CTLTBL_MAX(VAR) \
+ _Generic((VAR), signed int: INT_MAX, unsigned int: UINT_MAX, \
+ unsigned long: ULONG_MAX, default : ULONG_MAX)
+
+#define CTLTBL_ENTRY_V(VAR) \
+ __CTLTBL_ENTRY(VAR, #VAR, 0444, __CTLTBL_AUTO_HANDLER(VAR), \
+ __CTLTBL_MIN(VAR), __CTLTBL_MAX(VAR), sizeof(VAR))
+
+#define CTLTBL_ENTRY_VN(VAR, NAME) \
+ __CTLTBL_ENTRY(VAR, NAME, 0444, __CTLTBL_AUTO_HANDLER(VAR), \
+ __CTLTBL_MIN(VAR), __CTLTBL_MAX(VAR), sizeof(VAR))
+
+#define CTLTBL_ENTRY_VNM(VAR, NAME, MODE) \
+ __CTLTBL_ENTRY(VAR, NAME, MODE, __CTLTBL_AUTO_HANDLER(VAR), \
+ __CTLTBL_MIN(VAR), __CTLTBL_MAX(VAR), sizeof(VAR))
+
+#define CTLTBL_ENTRY_VNMH(VAR, NAME, MODE, HANDLER) \
+ __CTLTBL_ENTRY(VAR, NAME, MODE, HANDLER, \
+ __CTLTBL_MIN(VAR), __CTLTBL_MAX(VAR), sizeof(VAR))
+
+#define CTLTBL_ENTRY_VNMR(VAR, NAME, MODE, MIN, MAX) \
+ __CTLTBL_ENTRY(VAR, NAME, MODE, __CTLTBL_AUTO_HANDLER_MINMAX(VAR), \
+ MIN, MAX, sizeof(VAR))
+
+#define CTLTBL_ENTRY_VNMHR(VAR, NAME, MODE, HANDLER, MIN, MAX) \
+ __CTLTBL_ENTRY(VAR, NAME, MODE, HANDLER, MIN, MAX, sizeof(VAR))
+
+#define CTLTBL_ENTRY_VNMHRL(VAR, NAME, MODE, HANDLER, MIN, MAX, MAXLEN) \
+ __CTLTBL_ENTRY(VAR, NAME, MODE, HANDLER, MIN, MAX, MAXLEN)
+
+#define CTLTBL_ENTRY_NH(NAME, HANDLER) \
+ __CTLTBL_ENTRY_INIT(NULL, NAME, 0444, HANDLER, 0, ULONG_MAX, 0)
+
+#define CTLTBL_ENTRY_NMH(NAME, MODE, HANDLER) \
+ __CTLTBL_ENTRY_INIT(NULL, NAME, MODE, HANDLER, 0, ULONG_MAX, 0)
+
+#define CTLTBL_ENTRY_NMHL(NAME, MODE, HANDLER, MAXLEN) \
+ __CTLTBL_ENTRY_INIT(NULL, NAME, MODE, HANDLER, 0, ULONG_MAX, MAXLEN)
+
+#define CTLTBL_ENTRY_NMHRL(NAME, MODE, HANDLER, MIN, MAX, MAXLEN) \
+ __CTLTBL_ENTRY_INIT(NULL, NAME, MODE, HANDLER, MIN, MAX, MAXLEN)
+
#define KUNIT_PROC_READ 0
#define KUNIT_PROC_WRITE 1
+/*
+ * Test entries that carry handler context: data stays the real value storage
+ * and priv references the context object, as production handlers do.
+ */
+#define SYSCTL_TEST_CTX(VALUE, NAME, MODE, HANDLER, CTX) \
+ { \
+ .procname = (NAME), \
+ .data = &(VALUE), \
+ .maxlen = sizeof(VALUE), \
+ .mode = (MODE), \
+ .proc_handler = (HANDLER), \
+ .min = 0, \
+ .max = ULONG_MAX, \
+ .priv = &(CTX), \
+ }
+
+#define SYSCTL_TEST_CTX_NODATA(NAME, MODE, HANDLER, CTX) \
+ { \
+ .procname = (NAME), \
+ .maxlen = sizeof(int), \
+ .mode = (MODE), \
+ .proc_handler = (HANDLER), \
+ .min = 0, \
+ .max = ULONG_MAX, \
+ .priv = &(CTX), \
+ }
+
/*
* Test that proc_dointvec will not try to use a NULL .data field even when the
* length is non-zero.
*/
static void sysctl_test_api_dointvec_null_tbl_data(struct kunit *test)
{
- struct ctl_table null_data_table = {
- .procname = "foo",
- /*
- * Here we are testing that proc_dointvec behaves correctly when
- * we give it a NULL .data field. Normally this would point to a
- * piece of memory where the value would be stored.
- */
- .data = NULL,
- .maxlen = sizeof(int),
- .mode = 0644,
- .proc_handler = proc_dointvec,
- .extra1 = SYSCTL_ZERO,
- .extra2 = SYSCTL_ONE_HUNDRED,
- };
/*
- * proc_dointvec expects a buffer in user space, so we allocate one. We
- * also need to cast it to __user so sparse doesn't get mad.
+ * Here we are testing that proc_dointvec behaves correctly when
+ * we give it a NULL .data field. Normally this would point to a
+ * piece of memory where the value would be stored.
*/
- void __user *buffer = (void __user *)kunit_kzalloc(test, sizeof(int),
- GFP_USER);
+ const struct ctl_table null_data_table = CTLTBL_ENTRY_NMHRL("foo", 0644,
+ proc_dointvec, 0, 100, sizeof(int));
+ /*
+ * proc handlers consume kernel buffers; no userspace access is needed.
+ */
+ void *buffer = kunit_kzalloc(test, sizeof(int), GFP_KERNEL);
size_t len;
- loff_t pos;
+ loff_t pos = 0;
/*
* We don't care what the starting length is since proc_dointvec should
@@ -66,23 +173,16 @@ static void sysctl_test_api_dointvec_null_tbl_data(struct kunit *test)
static void sysctl_test_api_dointvec_table_maxlen_unset(struct kunit *test)
{
int data = 0;
- struct ctl_table data_maxlen_unset_table = {
- .procname = "foo",
- .data = &data,
- /*
- * So .data is no longer NULL, but we tell proc_dointvec its
- * length is 0, so it still shouldn't try to use it.
- */
- .maxlen = 0,
- .mode = 0644,
- .proc_handler = proc_dointvec,
- .extra1 = SYSCTL_ZERO,
- .extra2 = SYSCTL_ONE_HUNDRED,
- };
- void __user *buffer = (void __user *)kunit_kzalloc(test, sizeof(int),
- GFP_USER);
+ /*
+ * So .data is no longer NULL, but we tell proc_dointvec its
+ * length is 0, so it still shouldn't try to use it.
+ */
+ const struct ctl_table data_maxlen_unset_table = CTLTBL_ENTRY_VNMHRL(data,
+ "foo", 0644, proc_dointvec, 0, 100,
+ 0);
+ void *buffer = kunit_kzalloc(test, sizeof(int), GFP_KERNEL);
size_t len;
- loff_t pos;
+ loff_t pos = 0;
/*
* As before, we don't care what buffer length is because proc_dointvec
@@ -113,22 +213,14 @@ static void sysctl_test_api_dointvec_table_len_is_zero(struct kunit *test)
{
int data = 0;
/* Good table. */
- struct ctl_table table = {
- .procname = "foo",
- .data = &data,
- .maxlen = sizeof(int),
- .mode = 0644,
- .proc_handler = proc_dointvec,
- .extra1 = SYSCTL_ZERO,
- .extra2 = SYSCTL_ONE_HUNDRED,
- };
- void __user *buffer = (void __user *)kunit_kzalloc(test, sizeof(int),
- GFP_USER);
+ const struct ctl_table table = CTLTBL_ENTRY_VNMHR(data, "foo", 0644,
+ proc_dointvec, 0, 100);
+ void *buffer = kunit_kzalloc(test, sizeof(int), GFP_KERNEL);
/*
* However, now our read/write buffer has zero length.
*/
size_t len = 0;
- loff_t pos;
+ loff_t pos = 0;
KUNIT_EXPECT_EQ(test, 0, proc_dointvec(&table, KUNIT_PROC_READ, buffer,
&len, &pos));
@@ -147,17 +239,9 @@ static void sysctl_test_api_dointvec_table_read_but_position_set(
{
int data = 0;
/* Good table. */
- struct ctl_table table = {
- .procname = "foo",
- .data = &data,
- .maxlen = sizeof(int),
- .mode = 0644,
- .proc_handler = proc_dointvec,
- .extra1 = SYSCTL_ZERO,
- .extra2 = SYSCTL_ONE_HUNDRED,
- };
- void __user *buffer = (void __user *)kunit_kzalloc(test, sizeof(int),
- GFP_USER);
+ const struct ctl_table table = CTLTBL_ENTRY_VNMHR(data, "foo", 0644,
+ proc_dointvec, 0, 100);
+ void *buffer = kunit_kzalloc(test, sizeof(int), GFP_KERNEL);
/*
* We don't care about our buffer length because we start off with a
* non-zero file position.
@@ -182,24 +266,16 @@ static void sysctl_test_dointvec_read_happy_single_positive(struct kunit *test)
{
int data = 0;
/* Good table. */
- struct ctl_table table = {
- .procname = "foo",
- .data = &data,
- .maxlen = sizeof(int),
- .mode = 0644,
- .proc_handler = proc_dointvec,
- .extra1 = SYSCTL_ZERO,
- .extra2 = SYSCTL_ONE_HUNDRED,
- };
+ const struct ctl_table table = CTLTBL_ENTRY_VNMHR(data, "foo", 0644,
+ proc_dointvec, 0, 100);
size_t len = 4;
loff_t pos = 0;
- char *buffer = kunit_kzalloc(test, len, GFP_USER);
- char __user *user_buffer = (char __user *)buffer;
+ char *buffer = kunit_kzalloc(test, len, GFP_KERNEL);
/* Store 13 in the data field. */
*((int *)table.data) = 13;
KUNIT_EXPECT_EQ(test, 0, proc_dointvec(&table, KUNIT_PROC_READ,
- user_buffer, &len, &pos));
+ buffer, &len, &pos));
KUNIT_ASSERT_EQ(test, 3, len);
buffer[len] = '\0';
/* And we read 13 back out. */
@@ -213,23 +289,15 @@ static void sysctl_test_dointvec_read_happy_single_negative(struct kunit *test)
{
int data = 0;
/* Good table. */
- struct ctl_table table = {
- .procname = "foo",
- .data = &data,
- .maxlen = sizeof(int),
- .mode = 0644,
- .proc_handler = proc_dointvec,
- .extra1 = SYSCTL_ZERO,
- .extra2 = SYSCTL_ONE_HUNDRED,
- };
+ const struct ctl_table table = CTLTBL_ENTRY_VNMHR(data, "foo", 0644,
+ proc_dointvec, 0, 100);
size_t len = 5;
loff_t pos = 0;
- char *buffer = kunit_kzalloc(test, len, GFP_USER);
- char __user *user_buffer = (char __user *)buffer;
+ char *buffer = kunit_kzalloc(test, len, GFP_KERNEL);
*((int *)table.data) = -16;
KUNIT_EXPECT_EQ(test, 0, proc_dointvec(&table, KUNIT_PROC_READ,
- user_buffer, &len, &pos));
+ buffer, &len, &pos));
KUNIT_ASSERT_EQ(test, 4, len);
buffer[len] = '\0';
KUNIT_EXPECT_STREQ(test, "-16\n", buffer);
@@ -242,25 +310,17 @@ static void sysctl_test_dointvec_write_happy_single_positive(struct kunit *test)
{
int data = 0;
/* Good table. */
- struct ctl_table table = {
- .procname = "foo",
- .data = &data,
- .maxlen = sizeof(int),
- .mode = 0644,
- .proc_handler = proc_dointvec,
- .extra1 = SYSCTL_ZERO,
- .extra2 = SYSCTL_ONE_HUNDRED,
- };
+ const struct ctl_table table = CTLTBL_ENTRY_VNMHR(data, "foo", 0644,
+ proc_dointvec, 0, 100);
char input[] = "9";
size_t len = sizeof(input) - 1;
loff_t pos = 0;
- char *buffer = kunit_kzalloc(test, len, GFP_USER);
- char __user *user_buffer = (char __user *)buffer;
+ char *buffer = kunit_kzalloc(test, len, GFP_KERNEL);
memcpy(buffer, input, len);
KUNIT_EXPECT_EQ(test, 0, proc_dointvec(&table, KUNIT_PROC_WRITE,
- user_buffer, &len, &pos));
+ buffer, &len, &pos));
KUNIT_EXPECT_EQ(test, sizeof(input) - 1, len);
KUNIT_EXPECT_EQ(test, sizeof(input) - 1, pos);
KUNIT_EXPECT_EQ(test, 9, *((int *)table.data));
@@ -272,25 +332,17 @@ static void sysctl_test_dointvec_write_happy_single_positive(struct kunit *test)
static void sysctl_test_dointvec_write_happy_single_negative(struct kunit *test)
{
int data = 0;
- struct ctl_table table = {
- .procname = "foo",
- .data = &data,
- .maxlen = sizeof(int),
- .mode = 0644,
- .proc_handler = proc_dointvec,
- .extra1 = SYSCTL_ZERO,
- .extra2 = SYSCTL_ONE_HUNDRED,
- };
+ const struct ctl_table table = CTLTBL_ENTRY_VNMHR(data, "foo", 0644,
+ proc_dointvec, 0, 100);
char input[] = "-9";
size_t len = sizeof(input) - 1;
loff_t pos = 0;
- char *buffer = kunit_kzalloc(test, len, GFP_USER);
- char __user *user_buffer = (char __user *)buffer;
+ char *buffer = kunit_kzalloc(test, len, GFP_KERNEL);
memcpy(buffer, input, len);
KUNIT_EXPECT_EQ(test, 0, proc_dointvec(&table, KUNIT_PROC_WRITE,
- user_buffer, &len, &pos));
+ buffer, &len, &pos));
KUNIT_EXPECT_EQ(test, sizeof(input) - 1, len);
KUNIT_EXPECT_EQ(test, sizeof(input) - 1, pos);
KUNIT_EXPECT_EQ(test, -9, *((int *)table.data));
@@ -304,19 +356,11 @@ static void sysctl_test_api_dointvec_write_single_less_int_min(
struct kunit *test)
{
int data = 0;
- struct ctl_table table = {
- .procname = "foo",
- .data = &data,
- .maxlen = sizeof(int),
- .mode = 0644,
- .proc_handler = proc_dointvec,
- .extra1 = SYSCTL_ZERO,
- .extra2 = SYSCTL_ONE_HUNDRED,
- };
+ const struct ctl_table table = CTLTBL_ENTRY_VNMHR(data, "foo", 0644,
+ proc_dointvec, 0, 100);
size_t max_len = 32, len = max_len;
loff_t pos = 0;
- char *buffer = kunit_kzalloc(test, max_len, GFP_USER);
- char __user *user_buffer = (char __user *)buffer;
+ char *buffer = kunit_kzalloc(test, max_len, GFP_KERNEL);
unsigned long abs_of_less_than_min = (unsigned long)INT_MAX
- (INT_MAX + INT_MIN) + 1;
@@ -330,7 +374,7 @@ static void sysctl_test_api_dointvec_write_single_less_int_min(
max_len);
KUNIT_EXPECT_EQ(test, -EINVAL, proc_dointvec(&table, KUNIT_PROC_WRITE,
- user_buffer, &len, &pos));
+ buffer, &len, &pos));
KUNIT_EXPECT_EQ(test, max_len, len);
KUNIT_EXPECT_EQ(test, 0, *((int *)table.data));
}
@@ -342,19 +386,11 @@ static void sysctl_test_api_dointvec_write_single_greater_int_max(
struct kunit *test)
{
int data = 0;
- struct ctl_table table = {
- .procname = "foo",
- .data = &data,
- .maxlen = sizeof(int),
- .mode = 0644,
- .proc_handler = proc_dointvec,
- .extra1 = SYSCTL_ZERO,
- .extra2 = SYSCTL_ONE_HUNDRED,
- };
+ const struct ctl_table table = CTLTBL_ENTRY_VNMHR(data, "foo", 0644,
+ proc_dointvec, 0, 100);
size_t max_len = 32, len = max_len;
loff_t pos = 0;
- char *buffer = kunit_kzalloc(test, max_len, GFP_USER);
- char __user *user_buffer = (char __user *)buffer;
+ char *buffer = kunit_kzalloc(test, max_len, GFP_KERNEL);
unsigned long greater_than_max = (unsigned long)INT_MAX + 1;
KUNIT_ASSERT_GT(test, greater_than_max, (unsigned long)INT_MAX);
@@ -362,11 +398,340 @@ static void sysctl_test_api_dointvec_write_single_greater_int_max(
greater_than_max),
max_len);
KUNIT_EXPECT_EQ(test, -EINVAL, proc_dointvec(&table, KUNIT_PROC_WRITE,
- user_buffer, &len, &pos));
+ buffer, &len, &pos));
KUNIT_ASSERT_EQ(test, max_len, len);
KUNIT_EXPECT_EQ(test, 0, *((int *)table.data));
}
+/*
+ * priv is separate from data and is never interpreted by the core. Macro
+ * entries leave it NULL; a custom entry can attach typed context without
+ * displacing the value storage that standard handlers use.
+ */
+/* Invoke the selected handler directly, with kernel buffers and a fresh position. */
+static void sysctl_test_entry_roundtrip(struct kunit *test,
+ const struct ctl_table *table, const char *input)
+{
+ char expected[32];
+ char *buffer = kunit_kzalloc(test, sizeof(expected), GFP_KERNEL);
+ size_t len = strlen(input);
+ loff_t pos = 0;
+
+ KUNIT_ASSERT_NOT_NULL(test, buffer);
+ KUNIT_ASSERT_LT(test, len, sizeof(expected) - 1);
+ memcpy(buffer, input, len);
+ KUNIT_ASSERT_EQ(test, table->proc_handler(table, KUNIT_PROC_WRITE,
+ buffer, &len, &pos), 0);
+ KUNIT_EXPECT_EQ(test, len, strlen(input));
+ KUNIT_EXPECT_EQ(test, pos, strlen(input));
+
+ snprintf(expected, sizeof(expected), "%s\n", input);
+ memset(buffer, 0, sizeof(expected));
+ len = sizeof(expected) - 1;
+ pos = 0;
+ KUNIT_ASSERT_EQ(test, table->proc_handler(table, KUNIT_PROC_READ,
+ buffer, &len, &pos), 0);
+ KUNIT_ASSERT_LT(test, len, sizeof(expected));
+ buffer[len] = '\0';
+ KUNIT_EXPECT_EQ(test, len, strlen(expected));
+ KUNIT_EXPECT_STREQ(test, buffer, expected);
+}
+
+static int sysctl_test_owned_data;
+
+static int sysctl_test_owned_handler(const struct ctl_table *table, int dir,
+ void *buffer, size_t *lenp, loff_t *ppos)
+{
+ struct ctl_table resolved = *table;
+
+ resolved.data = &sysctl_test_owned_data;
+ resolved.maxlen = sizeof(sysctl_test_owned_data);
+ return proc_dointvec(&resolved, dir, buffer, lenp, ppos);
+}
+
+static void sysctl_test_entry_null_handler(struct kunit *test)
+{
+ const struct ctl_table table = CTLTBL_ENTRY_NH("owned", sysctl_test_owned_handler);
+
+ sysctl_test_owned_data = 0;
+ KUNIT_EXPECT_PTR_EQ(test, table.data, NULL);
+ KUNIT_EXPECT_EQ(test, table.maxlen, 0);
+ KUNIT_EXPECT_EQ(test, table.mode, 0444);
+ KUNIT_EXPECT_EQ(test, table.min, 0UL);
+ KUNIT_EXPECT_EQ(test, table.max, ULONG_MAX);
+ sysctl_test_entry_roundtrip(test, &table, "31");
+ KUNIT_EXPECT_EQ(test, sysctl_test_owned_data, 31);
+ sysctl_test_owned_data = 0;
+}
+
+static struct {
+ int vector[2];
+ int member;
+ char string[8];
+} sysctl_test_static_data;
+
+/* Both automatic and explicit-handler forms must allow static initializers. */
+static const struct ctl_table sysctl_test_static_entries[] = {
+ CTLTBL_ENTRY_VNMH(sysctl_test_static_data.vector, "vector", 0644, proc_dointvec),
+ CTLTBL_ENTRY_VNMH(sysctl_test_static_data.member, "member", 0644, proc_dointvec_minmax),
+ CTLTBL_ENTRY_VNMH(sysctl_test_static_data.string, "string", 0644, proc_dostring),
+ CTLTBL_ENTRY_VNMHRL(sysctl_test_static_data.vector, "first", 0644,
+ proc_dointvec, INT_MIN, INT_MAX, sizeof(int)),
+ CTLTBL_ENTRY_VNM(sysctl_test_static_data.member, "automatic", 0644),
+};
+
+static void sysctl_test_entry_explicit_handler(struct kunit *test)
+{
+ const struct ctl_table *entries = sysctl_test_static_entries;
+
+ memset(&sysctl_test_static_data, 0, sizeof(sysctl_test_static_data));
+ KUNIT_EXPECT_PTR_EQ(test, entries[0].data, &sysctl_test_static_data.vector);
+ KUNIT_EXPECT_EQ(test, entries[0].maxlen, sizeof(sysctl_test_static_data.vector));
+ KUNIT_EXPECT_PTR_EQ(test, entries[1].data, &sysctl_test_static_data.member);
+ KUNIT_EXPECT_EQ(test, entries[1].maxlen, sizeof(sysctl_test_static_data.member));
+ KUNIT_EXPECT_TRUE(test, entries[1].proc_handler == proc_dointvec_minmax);
+ KUNIT_EXPECT_PTR_EQ(test, entries[2].data, &sysctl_test_static_data.string);
+ KUNIT_EXPECT_EQ(test, entries[2].maxlen, sizeof(sysctl_test_static_data.string));
+ KUNIT_EXPECT_TRUE(test, entries[2].proc_handler == proc_dostring);
+ KUNIT_EXPECT_EQ(test, entries[3].maxlen, sizeof(int));
+ KUNIT_EXPECT_TRUE(test, entries[4].proc_handler == proc_dointvec);
+ KUNIT_EXPECT_EQ(test, entries[0].min, 0UL);
+ KUNIT_EXPECT_EQ(test, entries[0].max, ULONG_MAX);
+ KUNIT_EXPECT_EQ(test, entries[1].min, (unsigned long)INT_MIN);
+ KUNIT_EXPECT_EQ(test, entries[1].max, (unsigned long)INT_MAX);
+ KUNIT_EXPECT_EQ(test, entries[2].min, 0UL);
+ KUNIT_EXPECT_EQ(test, entries[2].max, ULONG_MAX);
+ KUNIT_EXPECT_EQ(test, entries[3].min, (unsigned long)INT_MIN);
+ KUNIT_EXPECT_EQ(test, entries[3].max, (unsigned long)INT_MAX);
+
+ sysctl_test_entry_roundtrip(test, &entries[0], "7\t8");
+ KUNIT_EXPECT_EQ(test, sysctl_test_static_data.vector[0], 7);
+ KUNIT_EXPECT_EQ(test, sysctl_test_static_data.vector[1], 8);
+ sysctl_test_entry_roundtrip(test, &entries[1], "-9");
+ KUNIT_EXPECT_EQ(test, sysctl_test_static_data.member, -9);
+ sysctl_test_entry_roundtrip(test, &entries[2], "hello");
+ KUNIT_EXPECT_STREQ(test, sysctl_test_static_data.string, "hello");
+ /* An explicit length limits the write without changing sizeof(vector). */
+ sysctl_test_entry_roundtrip(test, &entries[3], "11");
+ KUNIT_EXPECT_EQ(test, sysctl_test_static_data.vector[0], 11);
+ KUNIT_EXPECT_EQ(test, sysctl_test_static_data.vector[1], 8);
+ sysctl_test_entry_roundtrip(test, &entries[4], "-16");
+ KUNIT_EXPECT_EQ(test, sysctl_test_static_data.member, -16);
+}
+
+/* A rejected write must not change any byte of the backing storage. */
+static void sysctl_test_entry_reject(struct kunit *test,
+ const struct ctl_table *table, const char *input)
+{
+ void *saved = kunit_kmalloc(test, table->maxlen, GFP_KERNEL);
+ char *buffer = kunit_kstrdup(test, input, GFP_KERNEL);
+ size_t len = strlen(input);
+ loff_t pos = 0;
+
+ KUNIT_ASSERT_NOT_NULL(test, saved);
+ KUNIT_ASSERT_NOT_NULL(test, buffer);
+ memcpy(saved, table->data, table->maxlen);
+ KUNIT_EXPECT_EQ(test, table->proc_handler(table, KUNIT_PROC_WRITE,
+ buffer, &len, &pos), -EINVAL);
+ KUNIT_EXPECT_MEMEQ(test, table->data, saved, table->maxlen);
+}
+
+/* Entries are full-range, lower-only, upper-only and explicitly zero-only. */
+static void sysctl_test_entry_range_limits(struct kunit *test,
+ const struct ctl_table *entries,
+ const char *minimum, const char *maximum,
+ const char *underflow, const char *overflow)
+{
+ sysctl_test_entry_roundtrip(test, &entries[0], minimum);
+ sysctl_test_entry_roundtrip(test, &entries[0], maximum);
+ sysctl_test_entry_reject(test, &entries[0], underflow);
+ sysctl_test_entry_reject(test, &entries[0], overflow);
+
+ sysctl_test_entry_roundtrip(test, &entries[1], "2");
+ sysctl_test_entry_roundtrip(test, &entries[1], maximum);
+ sysctl_test_entry_reject(test, &entries[1], "1");
+ sysctl_test_entry_reject(test, &entries[1], overflow);
+
+ sysctl_test_entry_roundtrip(test, &entries[2], minimum);
+ sysctl_test_entry_roundtrip(test, &entries[2], "4");
+ sysctl_test_entry_reject(test, &entries[2], "5");
+ sysctl_test_entry_reject(test, &entries[2], underflow);
+
+ sysctl_test_entry_roundtrip(test, &entries[3], "0");
+ sysctl_test_entry_reject(test, &entries[3], "1");
+ sysctl_test_entry_reject(test, &entries[3], "-1");
+}
+
+static void sysctl_test_entry_int_limits(struct kunit *test)
+{
+ int data = 0;
+ const struct ctl_table entries[] = {
+ CTLTBL_ENTRY_VNMH(data, "full", 0644, proc_dointvec_minmax),
+ CTLTBL_ENTRY_VNMR(data, "lower", 0644, 2, INT_MAX),
+ CTLTBL_ENTRY_VNMR(data, "upper", 0644, INT_MIN, 4),
+ CTLTBL_ENTRY_VNMR(data, "zero", 0644, 0, 0),
+ };
+ char minimum[32], maximum[32], underflow[32], overflow[32];
+
+ snprintf(minimum, sizeof(minimum), "%d", INT_MIN);
+ snprintf(maximum, sizeof(maximum), "%d", INT_MAX);
+ snprintf(underflow, sizeof(underflow), "-%lu", (unsigned long)INT_MAX + 2);
+ snprintf(overflow, sizeof(overflow), "%lu", (unsigned long)INT_MAX + 1);
+ KUNIT_EXPECT_EQ(test, entries[0].min, (unsigned long)INT_MIN);
+ KUNIT_EXPECT_EQ(test, entries[0].max, (unsigned long)INT_MAX);
+ sysctl_test_entry_range_limits(test, entries, minimum, maximum, underflow, overflow);
+ KUNIT_EXPECT_EQ(test, data, 0);
+ sysctl_test_entry_roundtrip(test, &entries[0], minimum);
+ KUNIT_EXPECT_EQ(test, data, INT_MIN);
+ sysctl_test_entry_roundtrip(test, &entries[0], maximum);
+ KUNIT_EXPECT_EQ(test, data, INT_MAX);
+}
+
+static void sysctl_test_entry_ulong_limits(struct kunit *test)
+{
+ unsigned long data = 0;
+ const struct ctl_table entries[] = {
+ CTLTBL_ENTRY_VNMH(data, "full", 0644, proc_doulongvec_minmax),
+ CTLTBL_ENTRY_VNMR(data, "lower", 0644, 2, ULONG_MAX),
+ CTLTBL_ENTRY_VNMR(data, "upper", 0644, 0, 4),
+ CTLTBL_ENTRY_VNMR(data, "zero", 0644, 0, 0),
+ };
+ char maximum[32], overflow[32];
+
+ snprintf(maximum, sizeof(maximum), "%lu", ULONG_MAX);
+ /* Appending a digit overflows unsigned long on both 32-bit and 64-bit. */
+ snprintf(overflow, sizeof(overflow), "%lu0", ULONG_MAX);
+ KUNIT_EXPECT_EQ(test, entries[0].min, 0UL);
+ KUNIT_EXPECT_EQ(test, entries[0].max, ULONG_MAX);
+ sysctl_test_entry_range_limits(test, entries, "0", maximum, "-1", overflow);
+ KUNIT_EXPECT_EQ(test, data, 0UL);
+ sysctl_test_entry_roundtrip(test, &entries[0], maximum);
+ KUNIT_EXPECT_EQ(test, data, ULONG_MAX);
+}
+
+static void sysctl_test_entry_u8_limits(struct kunit *test)
+{
+ u8 data = 0;
+ const struct ctl_table entries[] = {
+ CTLTBL_ENTRY_VNMHR(data, "full", 0644, proc_dou8vec_minmax, 0, U8_MAX),
+ CTLTBL_ENTRY_VNMHR(data, "lower", 0644, proc_dou8vec_minmax, 2, U8_MAX),
+ CTLTBL_ENTRY_VNMHR(data, "upper", 0644, proc_dou8vec_minmax, 0, 4),
+ CTLTBL_ENTRY_VNMHR(data, "zero", 0644, proc_dou8vec_minmax, 0, 0),
+ CTLTBL_ENTRY_VNMHR(data, "both", 0644, proc_dou8vec_minmax, 2, 4),
+ };
+ char maximum[32], overflow[32];
+
+ snprintf(maximum, sizeof(maximum), "%u", U8_MAX);
+ snprintf(overflow, sizeof(overflow), "%u", U8_MAX + 1);
+ sysctl_test_entry_range_limits(test, entries, "0", maximum, "-1", overflow);
+ sysctl_test_entry_roundtrip(test, &entries[0], maximum);
+ KUNIT_EXPECT_EQ(test, data, U8_MAX);
+ sysctl_test_entry_roundtrip(test, &entries[4], "2");
+ KUNIT_EXPECT_EQ(test, data, 2);
+ sysctl_test_entry_roundtrip(test, &entries[4], "4");
+ KUNIT_EXPECT_EQ(test, data, 4);
+ sysctl_test_entry_reject(test, &entries[4], "1");
+ sysctl_test_entry_reject(test, &entries[4], "5");
+}
+
+static void sysctl_test_entry_array_no_partial_commit(struct kunit *test)
+{
+ int signed_data[] = { 2, 3 };
+ unsigned long long_data[] = { 2, 3 };
+ const struct ctl_table entries[] = {
+ CTLTBL_ENTRY_VNMHR(signed_data, "signed", 0644, proc_dointvec_minmax, 2, 4),
+ CTLTBL_ENTRY_VNMHR(long_data, "long", 0644, proc_doulongvec_minmax, 2, 4),
+ };
+ int i;
+
+ for (i = 0; i < ARRAY_SIZE(entries); i++) {
+ sysctl_test_entry_reject(test, &entries[i], "4\t5");
+ sysctl_test_entry_reject(test, &entries[i], "4\t1");
+ sysctl_test_entry_reject(test, &entries[i], "4\tbad");
+ sysctl_test_entry_reject(test, &entries[i], "4\t-1");
+ sysctl_test_entry_roundtrip(test, &entries[i], "4\t2");
+ }
+ KUNIT_EXPECT_EQ(test, signed_data[0], 4);
+ KUNIT_EXPECT_EQ(test, signed_data[1], 2);
+ KUNIT_EXPECT_EQ(test, long_data[0], 4UL);
+ KUNIT_EXPECT_EQ(test, long_data[1], 2UL);
+}
+
+/* Unsigned int and u8 handlers deliberately do not support arrays. */
+static void sysctl_test_entry_unsigned_arrays_rejected(struct kunit *test)
+{
+ unsigned int unsigned_data[] = { 2, 3 };
+ u8 byte_data[] = { 2, 3 };
+ const struct ctl_table entries[] = {
+ CTLTBL_ENTRY_VNMHR(unsigned_data, "unsigned", 0644,
+ proc_douintvec_minmax, 0, UINT_MAX),
+ CTLTBL_ENTRY_VNMHR(byte_data, "byte", 0644, proc_dou8vec_minmax, 0, U8_MAX),
+ };
+ int i;
+
+ for (i = 0; i < ARRAY_SIZE(entries); i++) {
+ char buffer[32] = {};
+ size_t len = sizeof(buffer);
+ loff_t pos = 0;
+
+ sysctl_test_entry_reject(test, &entries[i], "4\t2");
+ KUNIT_EXPECT_EQ(test, entries[i].proc_handler(&entries[i], KUNIT_PROC_READ,
+ buffer, &len, &pos), -EINVAL);
+ }
+}
+
+/* Each wrapper reads the related bound anew and resolves data in a local copy. */
+/* Context handlers resolve real storage on a local table; data is not a value. */
+/*
+ * Standard handlers only touch data and maxlen. Attaching priv to such an
+ * entry must not change what is stored, so a stray context pointer cannot
+ * redirect a write to the wrong address.
+ */
+/*
+ * Registration refuses priv on a standard handler: the context would be
+ * silently ignored, which is always a configuration mistake.
+ */
+
+/*
+ * A dynamic range wrapper reads its related limit through priv on each call
+ * while data keeps pointing at the value, mirroring addrconf and devinet.
+ */
+/*
+ * Dynamic range families refresh their related limit on every call. These
+ * generic wrappers model the delegation pattern used by scheduler, IPC,
+ * fragment, namespace and jiffies sysctls.
+ */
+/*
+ * Sibling thresholds bound each other. These mirror the fragment and SCTP
+ * handlers that read the other sysctl's current value at every call.
+ */
+
+/* Two object pointers supply value and limit without becoming bounds. */
+/* An owner embedded around the bound storage can be recovered with container_of(). */
+/*
+ * A registered table may be unregistered and recreated. The table and its
+ * handler context are caller-owned; registration must not rewrite either.
+ */
+/*
+ * Registration copies the table entry but must leave data and priv pointing
+ * at the caller-owned objects; the handler resolves storage through data and
+ * context through priv on every call.
+ */
+#if IS_ENABLED(CONFIG_NET) && IS_ENABLED(CONFIG_SYSCTL)
+/*
+ * Simulate per-net table duplication and teardown through the real
+ * register_net_sysctl_sz() path. This does not create a namespace, but it
+ * exercises copied tables, independent contexts, registration failure and
+ * unregister/re-register ordering.
+ */
+#endif
+
+/*
+ * Registered-entry harness. A static handler cannot be called by symbol from
+ * KUnit, but a registered table exposes its proc_handler through the entry.
+ * This helper is deliberately table-agnostic: the demo below uses a synthetic
+ * table, while future tests can pass a header obtained from a real subsystem.
+ */
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),
@@ -378,6 +743,15 @@ 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_entry_null_handler),
+ KUNIT_CASE(sysctl_test_entry_explicit_handler),
+ KUNIT_CASE(sysctl_test_entry_int_limits),
+ KUNIT_CASE(sysctl_test_entry_ulong_limits),
+ KUNIT_CASE(sysctl_test_entry_u8_limits),
+ KUNIT_CASE(sysctl_test_entry_array_no_partial_commit),
+ KUNIT_CASE(sysctl_test_entry_unsigned_arrays_rejected),
+#if IS_ENABLED(CONFIG_NET) && IS_ENABLED(CONFIG_SYSCTL)
+#endif
{}
};
--
2.25.1
next prev parent reply other threads:[~2026-10-28 16:35 UTC|newest]
Thread overview: 40+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-10-28 16:34 [RFC PATCH 00/39] sysctl: replace overloaded extra pointers with typed bounds and context wen.yang
2026-10-28 16:34 ` [RFC PATCH 01/39] " wen.yang
2026-10-28 16:34 ` [RFC PATCH 02/39] test_sysctl: convert bounds to numeric values wen.yang
2026-10-28 16:34 ` wen.yang [this message]
2026-10-28 16:34 ` [RFC PATCH 04/39] sysctl: cover runtime ranges and context lifecycles in KUnit wen.yang
2026-10-28 16:34 ` [RFC PATCH 05/39] sysctl: cover priv in KUnit and the debug test module wen.yang
2026-10-28 16:34 ` [RFC PATCH 06/39] sysctl: convert core table bounds to numeric values wen.yang
2026-10-28 16:34 ` [RFC PATCH 07/39] sysctl: store architecture sysctl bounds by value wen.yang
2026-10-28 16:34 ` [RFC PATCH 08/39] sysctl: store crypto " wen.yang
2026-10-28 16:34 ` [RFC PATCH 09/39] sysctl: store device " wen.yang
2026-10-28 16:34 ` [RFC PATCH 10/39] sysctl: store filesystem " wen.yang
2026-10-28 16:34 ` [RFC PATCH 11/39] sysctl: store message-queue " wen.yang
2026-10-28 16:34 ` [RFC PATCH 12/39] sysctl: store kernel " wen.yang
2026-10-28 16:34 ` [RFC PATCH 13/39] sysctl: store memory-management " wen.yang
2026-10-28 16:34 ` [RFC PATCH 14/39] sysctl: store IPv6 scalar " wen.yang
2026-10-28 16:34 ` [RFC PATCH 15/39] sysctl: store netfilter " wen.yang
2026-10-28 16:34 ` [RFC PATCH 16/39] sysctl: store RPC " wen.yang
2026-10-28 16:34 ` [RFC PATCH 17/39] sysctl: store remaining network " wen.yang
2026-10-28 16:34 ` [RFC PATCH 18/39] sysctl: store security " wen.yang
2026-10-28 16:34 ` [RFC PATCH 19/39] io_uring: store its " wen.yang
2026-10-28 16:34 ` [RFC PATCH 20/39] sysctl: refresh dynamic scheduler and PID bounds per call wen.yang
2026-10-28 16:35 ` [RFC PATCH 21/39] sysctl: refresh device-dependent " wen.yang
2026-10-28 16:35 ` [RFC PATCH 22/39] sysctl: refresh namespace IPC " wen.yang
2026-10-28 16:35 ` [RFC PATCH 23/39] yama: preserve scope locking with numeric sysctl bounds wen.yang
2026-10-28 16:35 ` [RFC PATCH 24/39] net: refresh core sysctl limits without bound pointers wen.yang
2026-10-28 16:35 ` [RFC PATCH 25/39] sctp: resolve numeric ranges from the current namespace wen.yang
2026-10-28 16:35 ` [RFC PATCH 26/39] rds: preserve live reconnect limits with numeric bounds wen.yang
2026-10-28 16:35 ` [RFC PATCH 27/39] ipv4: preserve live and vector bounds with numeric sysctls wen.yang
2026-10-28 16:35 ` [RFC PATCH 28/39] net: refresh fragment and remaining dynamic sysctl bounds wen.yang
2026-10-28 16:35 ` [RFC PATCH 29/39] neighbour: derive sysctl ownership independently of bounds wen.yang
2026-10-28 16:35 ` [RFC PATCH 30/39] net: resolve sysctl owners from their backing data wen.yang
2026-10-28 16:35 ` [RFC PATCH 31/39] ipvs: derive sysctl ownership from numeric storage wen.yang
2026-10-28 16:35 ` [RFC PATCH 32/39] sysctl: remove obsolete nr_open bound variables wen.yang
2026-10-28 16:35 ` [RFC PATCH 33/39] ipv6: move address sysctl context to priv wen.yang
2026-10-28 16:35 ` [RFC PATCH 34/39] ipv4: move device " wen.yang
2026-10-28 16:35 ` [RFC PATCH 35/39] netfilter: move logger " wen.yang
2026-10-28 16:35 ` [RFC PATCH 36/39] parport: move device " wen.yang
2026-10-28 16:35 ` [RFC PATCH 37/39] net: move namespace " wen.yang
2026-10-28 16:35 ` [RFC PATCH 38/39] arm64: move vector-length " wen.yang
2026-10-28 16:35 ` [RFC PATCH 39/39] sysctl: remove the extra pointers and the compatibility bounds read 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=3f6be87a6685418f170b6713c1759a9c16573871.1793204511.git.wen.yang@linux.dev \
--to=wen.yang@linux.dev \
--cc=joel.granados@kernel.org \
--cc=linux-fsdevel@vger.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®