Skip to contentSkip to conversation

sysctl: add typed field descriptors

PATCH Superseded Alexey Gladkov 2 participants 24 messages Sep 21 – Oct 23

Subsystems (3)
lore
Share & tools
lore
#0 lore raw

v2 cover sysctl: add typed field descriptors

Hi,

Some sysctl users allocate a private copy of an otherwise static ctl_table for every namespace or device, then rewrite data and limit pointers before registration. Besides the per-instance allocation, these fixups are often addressed by table index, so changing the table can silently associate an entry with the wrong data or limits.

This series adds struct sysctl_field as an alternative descriptor for such tables. A field records the value type and an offset into an object selected at registration time. The type-specific offset helpers are small wrappers around offsetof() and only add a compile-time check of the referenced member type. The sysctl core derives the handler, size, data and limits, and builds a temporary ctl_table when invoking existing handler, permission and BPF interfaces.

A struct sysctl_context identifies the object shared by the whole registration and is copied into the table header. Subsystems which need more than a namespace can embed it as the first member of a larger context and select the object to which field offsets apply.

Existing ctl_table users are unchanged, and subsystems can migrate one table at a time. Converted tables remain static and read-only instead of being copied and patched for every instance.

Compared with the previous RFC [1], this drops the per-field accessor functions and the macros which generated them. The first two patches prepare the sysctl core without changing its external behaviour. The third patch adds the new descriptor, followed by conversions of the IPC, mqueue and ucount tables as small users of the interface.

This is only the first part of the conversion. Network sysctls and the other subsystem-specific users will be submitted as separate follow-up series so they can be reviewed by their respective maintainers without making this initial series excessively large.

[1] https://lore.kernel.org/all/[email protected]/

Alexey Gladkov (6):
proc: sysctl: address table entries by index
sysctl: add unsigned int limit constants
sysctl: add typed field descriptors
sysctl: ipc: use typed fields for IPC namespace sysctls
sysctl: mq: use typed fields for IPC namespace sysctls
sysctl: use typed fields for ucount limits

 fs/proc/inode.c        |   2 +-
 fs/proc/internal.h     |   2 +-
 fs/proc/proc_sysctl.c  | 601 ++++++++++++++++++++++++++++++-----------
 include/linux/sysctl.h | 150 +++++++++-
 ipc/ipc_sysctl.c       | 188 ++++++-------
 ipc/mq_sysctl.c        | 104 +++----
 kernel/sysctl.c        |   3 +
 kernel/ucount.c        |  64 ++---
 8 files changed, 750 insertions(+), 364 deletions(-)

base-commit: 587858367581b9c55c3690f4e63382ad622719d4

Signature · 1 line
2.55.0
#1 lore raw

PATCH v2 1/6 proc: sysctl: address table entries by index

proc_sysctl keeps pointers to ctl_table entries in several lookup and inode paths. That makes it hard to support alternative descriptor formats because the tree logic assumes that every registered entry already is a ctl_table object.

Store the entry index in proc inodes and pass header/index pairs through the lookup, iteration and permission paths instead. The existing ctl_table entries are still used for all handler, permission and poll operations, but the core no longer needs to carry a direct ctl_table pointer through every internal path.

---
 fs/proc/inode.c       |   2 +-
 fs/proc/internal.h    |   2 +-
 fs/proc/proc_sysctl.c | 330 ++++++++++++++++++++++++------------------
 3 files changed, 194 insertions(+), 140 deletions(-)
fs/proc/inode.c +1 -1
index b7634f975d98..c735338ecc8d 100644
@@ -62,7 +62,7 @@ static struct inode *proc_alloc_inode(struct super_block *sb)
 	ei->op.proc_get_link = NULL;
 	ei->pde = NULL;
 	ei->sysctl = NULL;
-	ei->sysctl_entry = NULL;
+	ei->sysctl_entry_idx = 0;
 	INIT_HLIST_NODE(&ei->sibling_inodes);
 	ei->ns_ops = NULL;
 	return &ei->vfs_inode;
fs/proc/internal.h +1 -1
index 04bd6c9e65a7..7fc730b0d7ab 100644
@@ -123,7 +123,7 @@ struct proc_inode {
 	union proc_op op;
 	struct proc_dir_entry *pde;
 	struct ctl_table_header *sysctl;
-	const struct ctl_table *sysctl_entry;
+	size_t sysctl_entry_idx;
 	struct hlist_node sibling_inodes;
 	const struct proc_ns_operations *ns_ops;
 	struct inode vfs_inode;
fs/proc/proc_sysctl.c +192 -138

763 lines not on this page · raw message

Signature · 1 line
2.55.0
#2 lore raw

PATCH v2 2/6 sysctl: add unsigned int limit constants

In reply to Alexey Gladkov

Some sysctl handlers use unsigned int storage for their limit arguments. In particular, proc_dou8vec_minmax() expects extra1 and extra2 to point to unsigned int values even though the controlled data is an u8.

Provide shared unsigned int constants so typed sysctl descriptors can pass correctly typed min and max pointers without casting the existing int constants.

---
 include/linux/sysctl.h | 7 +++++++
 kernel/sysctl.c        | 3 +++
 2 files changed, 10 insertions(+)
include/linux/sysctl.h +7 -0
index e5d7226ab6f5..7139a4c72736 100644
@@ -59,6 +59,12 @@ extern const int sysctl_vals[];
 #define SYSCTL_LONG_ONE		((void *)&sysctl_long_vals[1])
 #define SYSCTL_LONG_MAX		((void *)&sysctl_long_vals[2])
 
+#define SYSCTL_UINT_ZERO	((unsigned int *)&sysctl_uint_vals[0])
+#define SYSCTL_UINT_ONE		((unsigned int *)&sysctl_uint_vals[1])
+#define SYSCTL_UINT_TWO		((unsigned int *)&sysctl_uint_vals[2])
+#define SYSCTL_UINT_THREE	((unsigned int *)&sysctl_uint_vals[3])
+#define SYSCTL_UINT_FOUR	((unsigned int *)&sysctl_uint_vals[4])
+
 /*
  *
  * "dir" originates from read_iter (dir = 0) or write_iter (dir = 1)
@@ -73,6 +79,7 @@ extern const int sysctl_vals[];
 #define SYSCTL_KERN_TO_USER(dir) (!dir)
 
 extern const unsigned long sysctl_long_vals[];
+extern const unsigned int sysctl_uint_vals[];
 
 typedef int proc_handler(const struct ctl_table *ctl, int dir, void *buf,
 			 size_t *lenp, loff_t *ppos);
kernel/sysctl.c +3 -0
index f7b75985d542..54edaa2fd5d5 100644
@@ -29,6 +29,9 @@ EXPORT_SYMBOL(sysctl_vals);
 const unsigned long sysctl_long_vals[] = { 0, 1, LONG_MAX };
 EXPORT_SYMBOL_GPL(sysctl_long_vals);
 
+const unsigned int sysctl_uint_vals[] = { 0, 1, 2, 3, 4 };
+EXPORT_SYMBOL_GPL(sysctl_uint_vals);
+
 #if defined(CONFIG_SYSCTL)
 
 /* Constants used for minimum and maximum */
Signature · 1 line
2.55.0
maintainer
#3 lore raw

Re: [PATCH v2 2/6] sysctl: add unsigned int limit constants

Quoted 3 lines · Alexey Gladkov

Some sysctl handlers use unsigned int storage for their limit arguments. In particular, proc_dou8vec_minmax() expects extra1 and extra2 to point to unsigned int values even though the controlled data is an u8.

This one reads like a fix as opposed to part of your series. Is it strictly needed for what you are proposing here?

Quoted 16 lines

Provide shared unsigned int constants so typed sysctl descriptors can pass correctly typed min and max pointers without casting the existing int constants.

---
 include/linux/sysctl.h | 7 +++++++
 kernel/sysctl.c        | 3 +++
 2 files changed, 10 insertions(+)
include/linux/sysctl.h +0 -0
index e5d7226ab6f5..7139a4c72736 100644

include/linux/sysctl.h extern const int sysctl_vals[]; · line 66

Quoted 8 lines
include/linux/sysctl.h +5 -0
@@ -59,6 +59,12 @@ extern const int sysctl_vals[];
 #define SYSCTL_LONG_ONE		((void *)&sysctl_long_vals[1])
 #define SYSCTL_LONG_MAX		((void *)&sysctl_long_vals[2])
 
+#define SYSCTL_UINT_ZERO	((unsigned int *)&sysctl_uint_vals[0])
+#define SYSCTL_UINT_ONE		((unsigned int *)&sysctl_uint_vals[1])
+#define SYSCTL_UINT_TWO		((unsigned int *)&sysctl_uint_vals[2])
+#define SYSCTL_UINT_THREE	((unsigned int *)&sysctl_uint_vals[3])
+#define SYSCTL_UINT_FOUR	((unsigned int *)&sysctl_uint_vals[4])

I would really like to avoid that. Its one of the things that should probably go away in the future [1], [2], [3], [4]

[1] https://lore.kernel.org/all/[email protected]/
[2] https://lore.kernel.org/all/[email protected]/
[3] https://lore.kernel.org/all/[email protected]/
[4] https://lore.kernel.org/all/qnrzl4tjlgw5rzlvxavr3pt7fhkslnm4dd62q7uqzb3mfoa2jg@fuayx77rfcs6/

Quoted 29 lines
include/linux/sysctl.h +2 -0
+
 /*
  *
  * "dir" originates from read_iter (dir = 0) or write_iter (dir = 1)
@@ -73,6 +79,7 @@ extern const int sysctl_vals[];
 #define SYSCTL_KERN_TO_USER(dir) (!dir)
 
 extern const unsigned long sysctl_long_vals[];
+extern const unsigned int sysctl_uint_vals[];
 
 typedef int proc_handler(const struct ctl_table *ctl, int dir, void *buf,
 			 size_t *lenp, loff_t *ppos);
kernel/sysctl.c +3 -0
index f7b75985d542..54edaa2fd5d5 100644
@@ -29,6 +29,9 @@ EXPORT_SYMBOL(sysctl_vals);
 const unsigned long sysctl_long_vals[] = { 0, 1, LONG_MAX };
 EXPORT_SYMBOL_GPL(sysctl_long_vals);
 
+const unsigned int sysctl_uint_vals[] = { 0, 1, 2, 3, 4 };
+EXPORT_SYMBOL_GPL(sysctl_uint_vals);
+
 #if defined(CONFIG_SYSCTL)
 
 /* Constants used for minimum and maximum */
Signature · 1 line
2.55.0
#4 lore raw

Re: [PATCH v2 2/6] sysctl: add unsigned int limit constants

Quoted 7 lines · Joel Granados
Quoted 3 lines · Alexey Gladkov

Some sysctl handlers use unsigned int storage for their limit arguments. In particular, proc_dou8vec_minmax() expects extra1 and extra2 to point to unsigned int values even though the controlled data is an u8.

This one reads like a fix as opposed to part of your series. Is it strictly needed for what you are proposing here?

Yeah, it turned out a bit wonky. Sorry.

What I was trying to say is that we don't have shared constants for unsigned int. It doesn't matter right now since the size is the same, but if we check the type at compile time, then it becomes important.

Quoted 28 lines
Quoted 16 lines

Provide shared unsigned int constants so typed sysctl descriptors can pass correctly typed min and max pointers without casting the existing int constants.

---
 include/linux/sysctl.h | 7 +++++++
 kernel/sysctl.c        | 3 +++
 2 files changed, 10 insertions(+)
include/linux/sysctl.h +0 -0
index e5d7226ab6f5..7139a4c72736 100644

include/linux/sysctl.h extern const int sysctl_vals[]; · line 66

Quoted 8 lines
include/linux/sysctl.h +5 -0
@@ -59,6 +59,12 @@ extern const int sysctl_vals[];
 #define SYSCTL_LONG_ONE		((void *)&sysctl_long_vals[1])
 #define SYSCTL_LONG_MAX		((void *)&sysctl_long_vals[2])
 
+#define SYSCTL_UINT_ZERO	((unsigned int *)&sysctl_uint_vals[0])
+#define SYSCTL_UINT_ONE		((unsigned int *)&sysctl_uint_vals[1])
+#define SYSCTL_UINT_TWO		((unsigned int *)&sysctl_uint_vals[2])
+#define SYSCTL_UINT_THREE	((unsigned int *)&sysctl_uint_vals[3])
+#define SYSCTL_UINT_FOUR	((unsigned int *)&sysctl_uint_vals[4])

I would really like to avoid that. Its one of the things that should probably go away in the future [1], [2], [3], [4]

Quoted 4 lines

[1] https://lore.kernel.org/all/[email protected]/
[2] https://lore.kernel.org/all/[email protected]/
[3] https://lore.kernel.org/all/[email protected]/
[4] https://lore.kernel.org/all/qnrzl4tjlgw5rzlvxavr3pt7fhkslnm4dd62q7uqzb3mfoa2jg@fuayx77rfcs6/

I don't mind removing them and using constants in the relevant source files. I added these constants because they are used in the net subsystem.

As example:

https://web.git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/net/mptcp/ctrl.c#n294
https://web.git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/ipc/ipc_sysctl.c#n110

Quoted 30 lines
Quoted 30 lines
include/linux/sysctl.h +2 -0
+
 /*
  *
  * "dir" originates from read_iter (dir = 0) or write_iter (dir = 1)
@@ -73,6 +79,7 @@ extern const int sysctl_vals[];
 #define SYSCTL_KERN_TO_USER(dir) (!dir)
 
 extern const unsigned long sysctl_long_vals[];
+extern const unsigned int sysctl_uint_vals[];
 
 typedef int proc_handler(const struct ctl_table *ctl, int dir, void *buf,
 			 size_t *lenp, loff_t *ppos);
kernel/sysctl.c +3 -0
index f7b75985d542..54edaa2fd5d5 100644
@@ -29,6 +29,9 @@ EXPORT_SYMBOL(sysctl_vals);
 const unsigned long sysctl_long_vals[] = { 0, 1, LONG_MAX };
 EXPORT_SYMBOL_GPL(sysctl_long_vals);
 
+const unsigned int sysctl_uint_vals[] = { 0, 1, 2, 3, 4 };
+EXPORT_SYMBOL_GPL(sysctl_uint_vals);
+
 #if defined(CONFIG_SYSCTL)
 
 /* Constants used for minimum and maximum */
Signature · 1 line
2.55.0
Signature · 1 line
Rgrds, legion
#5 lore raw

PATCH v2 3/6 sysctl: add typed field descriptors

In reply to Alexey Gladkov

Several sysctl users duplicate ctl_table arrays at registration time so data and limit pointers can be redirected to namespace or device state. The copies consume memory for every instance and their index-based fixups silently depend on the source table order.

Add sysctl_field as an alternative static descriptor. A field records the value kind and a checked offset into an object selected by a registration context. Type-specific offset helpers verify the backing member type at build time, while the core derives the legacy proc handler, size and limit pointers from the field kind.

Keep ctl_table as the interface used by proc handlers, permissions and BPF by materializing one entry on the stack when those paths need it. Existing ctl_table registrations are unchanged, and converted users can share one read-only descriptor array without allocating a table copy.

---
 fs/proc/proc_sysctl.c  | 289 +++++++++++++++++++++++++++++++++++++----
 include/linux/sysctl.h | 143 +++++++++++++++++++-
 2 files changed, 400 insertions(+), 32 deletions(-)
fs/proc/proc_sysctl.c +262 -27
index b5cb219bcdbc..73f3408b2043 100644
@@ -18,6 +18,7 @@
 #include <linux/mount.h>
 #include <linux/kmemleak.h>
 #include <linux/lockdep.h>
+#include <linux/overflow.h>
 #include "internal.h"
 
 #define list_for_each_table_entry(index, header)			\
@@ -91,28 +92,178 @@ static int sysctl_follow_link(struct ctl_table_header **phead, size_t *pindex);
 static int insert_links(struct ctl_table_header *head);
 static void put_links(struct ctl_table_header *header);
 
+static inline bool is_field_table(const struct ctl_table_header *head)
+{
+	return head->table_kind == SYSCTL_TABLE_KIND_FIELD;
+}
+
 static const char *sysctl_entry_procname(struct ctl_table_header *head,
 					 size_t index)
 {
+	if (is_field_table(head))
+		return head->ctl_fields[index].procname;
+
 	return head->ctl_table[index].procname;
 }
 
 static umode_t sysctl_entry_mode(struct ctl_table_header *head, size_t index)
 {
-	return head->ctl_table[index].mode;
+	const struct sysctl_field *field;
+
+	if (!is_field_table(head))
+		return head->ctl_table[index].mode;
+
+	field = &head->ctl_fields[index];
+
+	if (field->mode_fn) {
+		lockdep_assert_not_held(&sysctl_lock);
+		return field->mode_fn(head->ctx);
+	}
+
+	return field->mode;
+}
+
+static bool sysctl_entry_is_dir(const struct ctl_table_header *head,
+				size_t index)
+{
+	/* Do not call field mode_fn callback while holding sysctl_lock. */
+	if (is_field_table(head))
+		return false;
+
+	return S_ISDIR(head->ctl_table[index].mode);
+}
+
+static bool sysctl_entry_is_link(const struct ctl_table_header *head,
+				 size_t index)
+{
+	if (is_field_table(head))
+		return false;
+
+	return S_ISLNK(head->ctl_table[index].mode);
 }
 
 static struct ctl_table_poll *sysctl_entry_poll(struct ctl_table_header *head,
 						size_t index)
 {
+	if (is_field_table(head))
+		return NULL;
+
 	return head->ctl_table[index].poll;
 }
 
+static void *sysctl_context_object(const struct sysctl_context *ctx)
+{
+	if (!ctx)
+		return NULL;
+	if (ctx->object)
+		return ctx->object(ctx);
+
+	switch (ctx->type) {
+	case SYSCTL_CONTEXT_USER_NS:
+		return ctx->ns.user_ns;
+	case SYSCTL_CONTEXT_IPC_NS:
+		return ctx->ns.ipc_ns;
+	case SYSCTL_CONTEXT_PID_NS:
+		return ctx->ns.pid_ns;
+	case SYSCTL_CONTEXT_NET_NS:
+		return ctx->ns.net_ns;
+	}
+
+	return NULL;
+}
+
+static void *sysctl_context_data(const struct sysctl_context *ctx,
+				 size_t offset, size_t size)
+{
+	void *object = sysctl_context_object(ctx);
+
+	if (!object || offset > ctx->object_size ||
+	    size > ctx->object_size - offset)
+		return NULL;
+
+	return (char *)object + offset;
+}
+
 static const struct ctl_table *
 sysctl_entry_table(struct ctl_table_header *head, size_t index,
 		   struct ctl_table *table)
 {
-	return &head->ctl_table[index];
+	const struct sysctl_field *field;
+
+	if (!is_field_table(head))
+		return &head->ctl_table[index];
+
+	field = &head->ctl_fields[index];
+
+	memset(table, 0, sizeof(*table));
+	table->procname = field->procname;
+	table->mode = field->mode;
+
+	if (field->mode_fn)
+		table->mode = field->mode_fn(head->ctx);
+
+	switch (field->type) {
+	case SYSCTL_FIELD_NO_DATA:
+		break;
+	case SYSCTL_FIELD_STRING:
+		table->proc_handler = proc_dostring;
+		table->maxlen = field->maxlen;
+		break;
+	case SYSCTL_FIELD_BOOL:
+		table->proc_handler = proc_dobool;
+		table->maxlen = sizeof(bool);
+		break;
+	case SYSCTL_FIELD_U8:
+	case SYSCTL_FIELD_U8_MINMAX:
+		table->proc_handler = proc_dou8vec_minmax;
+		table->maxlen = sizeof(u8);
+		table->extra1 = field->u8_limits.min;
+		table->extra2 = field->u8_limits.max;
+		break;
+	case SYSCTL_FIELD_INT:
+	case SYSCTL_FIELD_INT_MINMAX:
+		table->proc_handler = field->type == SYSCTL_FIELD_INT ?
+				      proc_dointvec : proc_dointvec_minmax;
+		table->maxlen = sizeof(int);
+		table->extra1 = field->int_limits.min;
+		table->extra2 = field->int_limits.max;
+		break;
+	case SYSCTL_FIELD_UINT:
+	case SYSCTL_FIELD_UINT_MINMAX:
+		table->proc_handler = field->type == SYSCTL_FIELD_UINT ?
+				      proc_douintvec : proc_douintvec_minmax;
+		table->maxlen = sizeof(unsigned int);
+		table->extra1 = field->uint_limits.min;
+		table->extra2 = field->uint_limits.max;
+		break;
+	case SYSCTL_FIELD_LONG:
+	case SYSCTL_FIELD_LONG_MINMAX:
+		table->proc_handler = proc_doulongvec_minmax;
+		table->maxlen = sizeof(long);
+		table->extra1 = field->long_limits.min;
+		table->extra2 = field->long_limits.max;
+		break;
+	case SYSCTL_FIELD_ULONG:
+	case SYSCTL_FIELD_ULONG_MINMAX:
+		table->proc_handler = proc_doulongvec_minmax;
+		table->maxlen = sizeof(unsigned long);
+		table->extra1 = field->ulong_limits.min;
+		table->extra2 = field->ulong_limits.max;
+		break;
+	case SYSCTL_FIELD_SIZE_T:
+		table->proc_handler = proc_doulongvec_minmax;
+		table->maxlen = sizeof(size_t);
+		break;
+	}
+
+	if (field->type != SYSCTL_FIELD_NO_DATA)
+		table->data = sysctl_context_data(head->ctx, field->data_offset, table->maxlen);
+	if (field->proc_handler)
+		table->proc_handler = field->proc_handler;
+	if (field->maxlen)
+		table->maxlen = field->maxlen;
+
+	return table;
 }
 
 static void sysctl_print_dir(struct ctl_dir *dir)
@@ -212,9 +363,17 @@ static void erase_entry(struct ctl_table_header *head, size_t index)
 
 static void init_header(struct ctl_table_header *head,
 	struct ctl_table_root *root, struct ctl_table_set *set,
-	struct ctl_node *node, const struct ctl_table *table, size_t table_size)
+	struct ctl_node *node, const struct ctl_table *table,
+	const struct sysctl_field *fields, size_t table_size,
+	const struct sysctl_context *ctx)
 {
-	head->ctl_table = table;
+	if (fields) {
+		head->ctl_fields = fields;
+		head->table_kind = SYSCTL_TABLE_KIND_FIELD;
+	} else {
+		head->ctl_table = table;
+		head->table_kind = SYSCTL_TABLE_KIND_TABLE;
+	}
 	head->ctl_table_size = table_size;
 	head->ctl_table_arg = table;
 	head->used = 0;
@@ -223,6 +382,7 @@ static void init_header(struct ctl_table_header *head,
 	head->unregistering = NULL;
 	head->root = root;
 	head->set = set;
+	head->ctx = ctx;
 	head->parent = NULL;
 	head->node = node;
 	INIT_HLIST_HEAD(&head->inodes);
@@ -981,7 +1141,7 @@ static struct ctl_dir *find_subdir(struct ctl_dir *dir,
 
 	if (!find_entry(&head, &index, dir, name, namelen))
 		return ERR_PTR(-ENOENT);
-	if (!S_ISDIR(sysctl_entry_mode(head, index)))
+	if (!sysctl_entry_is_dir(head, index))
 		return ERR_PTR(-ENOTDIR);
 	return container_of(head, struct ctl_dir, header);
 }
@@ -1006,7 +1166,8 @@ static struct ctl_dir *new_dir(struct ctl_table_set *set,
 	memcpy(new_name, name, namelen);
 	table[0].procname = new_name;
 	table[0].mode = S_IFDIR|S_IRUGO|S_IXUGO;
-	init_header(&new->header, set->dir.header.root, set, node, table, 1);
+	init_header(&new->header, set->dir.header.root, set, node, table, NULL,
+		    1, NULL);
 
 	return new;
 }
@@ -1187,6 +1348,10 @@ static int sysctl_check_table(const char *path, struct ctl_table_header *header)
 		entry = sysctl_entry_table(header, index, &table);
 		if (!entry->procname)
 			err |= sysctl_err(path, entry, "procname is null");
+		if (is_field_table(header) &&
+		    header->ctl_fields[index].type != SYSCTL_FIELD_NO_DATA &&
+		    !entry->data)
+			err |= sysctl_err(path, entry, "No data");
 		if ((entry->proc_handler == proc_dostring) ||
 		    (entry->proc_handler == proc_dobool) ||
 		    (entry->proc_handler == proc_dointvec) ||
@@ -1199,7 +1364,7 @@ static int sysctl_check_table(const char *path, struct ctl_table_header *header)
 		    (entry->proc_handler == proc_dointvec_ms_jiffies) ||
 		    (entry->proc_handler == proc_doulongvec_minmax) ||
 		    (entry->proc_handler == proc_doulongvec_ms_jiffies_minmax)) {
-			if (!entry->data)
+			if (!is_field_table(header) && !entry->data)
 				err |= sysctl_err(path, entry, "No data");
 			if (!entry->maxlen)
 				err |= sysctl_err(path, entry, "No maxlen");
@@ -1255,7 +1420,7 @@ static struct ctl_table_header *new_links(struct ctl_dir *dir, struct ctl_table_
 		link++;
 	}
 	init_header(links, dir->header.root, dir->header.set, node, link_table,
-		    head->ctl_table_size);
+		    NULL, head->ctl_table_size, NULL);
 	links->nreg = head->ctl_table_size;
 
 	return links;
@@ -1279,10 +1444,10 @@ static bool get_links(struct ctl_dir *dir,
 		if (!find_entry(&tmp_head, &link_index, dir, procname,
 				strlen(procname)))
 			return false;
-		if (S_ISDIR(sysctl_entry_mode(tmp_head, link_index)) &&
-		    S_ISDIR(sysctl_entry_mode(header, index)))
+		if (sysctl_entry_is_dir(tmp_head, link_index) &&
+		    sysctl_entry_is_dir(header, index))
 			continue;
-		if (S_ISLNK(sysctl_entry_mode(tmp_head, link_index)) &&
+		if (sysctl_entry_is_link(tmp_head, link_index) &&
 		    tmp_head->ctl_table[link_index].data == link_root)
 			continue;
 		return false;
@@ -1369,18 +1534,25 @@ static struct ctl_dir *sysctl_mkdir_p(struct ctl_dir *dir, const char *path)
 }
 
 /**
- * __register_sysctl_table - register a leaf sysctl table
+ * __register_sysctl_table_internal - register a leaf sysctl table
  * @set: Sysctl tree to register on
  * @path: The path to the directory the sysctl table is in.
+ * @table: The top-level ctl_table array, or %NULL when registering @fields.
+ * @fields: The top-level ctl_field array, or %NULL when registering @table.
+ * @table_size: The number of elements in @table or @fields.
+ * @ctx: Optional context used to resolve @fields entries.
+ * @ctx_size: Size of @ctx, including any wrapper object that embeds it.
  *
- * @table: the top-level table structure. This table should not be free'd
- *         after registration. So it should not be used on stack. It can either
- *         be a global or dynamically allocated by the caller and free'd later
- *         after sysctl unregistration.
- * @table_size : The number of elements in table
+ * Register a sysctl table hierarchy. One of @table or @fields must be
+ * provided. The descriptor array should not be freed after registration, so it
+ * should not be used on stack. It can either be global or dynamically
+ * allocated by the caller and freed later after sysctl unregistration.
  *
- * Register a sysctl table hierarchy. @table should be a filled in ctl_table
- * array.
+ * If @ctx points to a wrapper object, &struct sysctl_context must be the first
+ * member so @ctx can be copied together with the rest of that object.
+ * Data offsets stored in @fields are checked against @ctx->object_size and
+ * applied to the namespace selected by @ctx->ns. A context wrapper may provide
+ * @ctx->object for tables whose data belongs to another object.
  *
  * The members of the &struct ctl_table structure are used as follows:
  * procname - the name of the sysctl file under /proc/sys. Set to %NULL to not
@@ -1411,25 +1583,69 @@ static struct ctl_dir *sysctl_mkdir_p(struct ctl_dir *dir, const char *path)
  * This routine returns %NULL on a failure to register, and a pointer
  * to the table header on success.
  */
-struct ctl_table_header *__register_sysctl_table(
-	struct ctl_table_set *set,
-	const char *path, const struct ctl_table *table, size_t table_size)
+static struct ctl_table_header *
+__register_sysctl_table_internal(struct ctl_table_set *set, const char *path,
+				 const struct ctl_table *table,
+				 const struct sysctl_field *fields,
+				 size_t table_size,
+				 const struct sysctl_context *ctx, size_t ctx_size)
 {
 	struct ctl_table_root *root = set->dir.header.root;
 	struct ctl_table_header *header;
 	struct ctl_dir *dir;
 	struct ctl_node *node;
+	const struct sysctl_context *header_ctx = NULL;
+	size_t nodes_size;
 	size_t alloc_size;
+	size_t context_offset;
+
+	if (!!table == !!fields)
+		return NULL;
+
+	if (ctx && ctx_size < sizeof(*ctx))
+		return NULL;
+
+	if (!ctx && ctx_size)
+		return NULL;
+
+	if (fields && (!ctx || !ctx->object_size ||
+		       !sysctl_context_object(ctx)))
+		return NULL;
+
+	if (check_mul_overflow(sizeof(struct ctl_node), table_size, &nodes_size))
+		return NULL;
+
+	if (check_add_overflow(sizeof(*header), nodes_size, &context_offset))
+		return NULL;
+
+	/*
+	 * Store the copied context after the ctl_node array. struct sysctl_context
+	 * is the first member of any caller-defined wrapper, whose alignment
+	 * must not exceed that of struct sysctl_context.
+	 */
+	if (ctx) {
+		if (check_add_overflow(context_offset,
+				       __alignof__(*ctx) - 1, &context_offset))
+			return NULL;
+		context_offset = ALIGN_DOWN(context_offset, __alignof__(*ctx));
+	}
 
-	alloc_size = sizeof(struct ctl_table_header) +
-		     sizeof(struct ctl_node) * table_size;
+	if (check_add_overflow(context_offset, ctx_size, &alloc_size))
+		return NULL;
 
 	header = kzalloc(alloc_size, GFP_KERNEL_ACCOUNT);
 	if (!header)
 		return NULL;
 
 	node = (struct ctl_node *)(header + 1);
-	init_header(header, root, set, node, table, table_size);
+	if (ctx) {
+		header_ctx = (const struct sysctl_context *)((void *)header +
+							   context_offset);
+		memcpy((void *)header_ctx, ctx, ctx_size);
+	}
+
+	init_header(header, root, set, node, table, fields, table_size,
+		    header_ctx);
 	if (sysctl_check_table(path, header))
 		goto fail;
 
@@ -1459,6 +1675,25 @@ struct ctl_table_header *__register_sysctl_table(
 	return NULL;
 }
 
+struct ctl_table_header *
+__register_sysctl_fields(struct ctl_table_set *set, const char *path,
+			 const struct sysctl_field *fields, size_t field_count,
+			 const struct sysctl_context *ctx, size_t ctx_size)
+{
+	return __register_sysctl_table_internal(set, path, NULL, fields,
+						field_count, ctx, ctx_size);
+}
+EXPORT_SYMBOL(__register_sysctl_fields);
+
+struct ctl_table_header *
+__register_sysctl_table(struct ctl_table_set *set, const char *path,
+			const struct ctl_table *table, size_t table_size)
+{
+	return __register_sysctl_table_internal(set, path, table, NULL,
+						table_size, NULL, 0);
+}
+EXPORT_SYMBOL(__register_sysctl_table);
+
 /**
  * register_sysctl_sz - register a sysctl table
  * @path: The path to the directory the sysctl table is in. If the path
@@ -1547,7 +1782,7 @@ static void put_links(struct ctl_table_header *header)
 
 		if (link &&
 		    ((S_ISDIR(link->mode) &&
-		      S_ISDIR(sysctl_entry_mode(header, index))) ||
+		      sysctl_entry_is_dir(header, index)) ||
 		     (S_ISLNK(link->mode) && (link->data == root)))) {
 			drop_sysctl_table(link_head);
 		} else {
@@ -1603,7 +1838,7 @@ void setup_sysctl_set(struct ctl_table_set *set,
 {
 	memset(set, 0, sizeof(*set));
 	set->is_seen = is_seen;
-	init_header(&set->dir.header, root, set, NULL, root_table, 1);
+	init_header(&set->dir.header, root, set, NULL, root_table, NULL, 1, NULL);
 }
 
 void retire_sysctl_set(struct ctl_table_set *set)
include/linux/sysctl.h +138 -5

222 lines not on this page · raw message

Signature · 1 line
2.55.0
maintainer
#6 lore raw
Quoted 105 lines · Alexey Gladkov

Several sysctl users duplicate ctl_table arrays at registration time so data and limit pointers can be redirected to namespace or device state. The copies consume memory for every instance and their index-based fixups silently depend on the source table order.

Add sysctl_field as an alternative static descriptor. A field records the value kind and a checked offset into an object selected by a registration context. Type-specific offset helpers verify the backing member type at build time, while the core derives the legacy proc handler, size and limit pointers from the field kind.

Keep ctl_table as the interface used by proc handlers, permissions and BPF by materializing one entry on the stack when those paths need it. Existing ctl_table registrations are unchanged, and converted users can share one read-only descriptor array without allocating a table copy.

---
 fs/proc/proc_sysctl.c  | 289 +++++++++++++++++++++++++++++++++++++----
 include/linux/sysctl.h | 143 +++++++++++++++++++-
 2 files changed, 400 insertions(+), 32 deletions(-)
fs/proc/proc_sysctl.c +51 -1
index b5cb219bcdbc..73f3408b2043 100644
@@ -18,6 +18,7 @@
 #include <linux/mount.h>
 #include <linux/kmemleak.h>
 #include <linux/lockdep.h>
+#include <linux/overflow.h>
 #include "internal.h"
 
 #define list_for_each_table_entry(index, header)			\
@@ -91,28 +92,178 @@ static int sysctl_follow_link(struct ctl_table_header **phead, size_t *pindex);
 static int insert_links(struct ctl_table_header *head);
 static void put_links(struct ctl_table_header *header);
 
+static inline bool is_field_table(const struct ctl_table_header *head)
+{
+	return head->table_kind == SYSCTL_TABLE_KIND_FIELD;
+}
+
 static const char *sysctl_entry_procname(struct ctl_table_header *head,
 					 size_t index)
 {
+	if (is_field_table(head))
+		return head->ctl_fields[index].procname;
+
 	return head->ctl_table[index].procname;
 }
 
 static umode_t sysctl_entry_mode(struct ctl_table_header *head, size_t index)
 {
-	return head->ctl_table[index].mode;
+	const struct sysctl_field *field;
+
+	if (!is_field_table(head))
+		return head->ctl_table[index].mode;
+
+	field = &head->ctl_fields[index];
+
+	if (field->mode_fn) {
+		lockdep_assert_not_held(&sysctl_lock);
+		return field->mode_fn(head->ctx);
+	}
+
+	return field->mode;
+}
+
+static bool sysctl_entry_is_dir(const struct ctl_table_header *head,
+				size_t index)
+{
+	/* Do not call field mode_fn callback while holding sysctl_lock. */
+	if (is_field_table(head))
+		return false;
+
+	return S_ISDIR(head->ctl_table[index].mode);
+}
+
+static bool sysctl_entry_is_link(const struct ctl_table_header *head,
+				 size_t index)
+{
+	if (is_field_table(head))
+		return false;
+
+	return S_ISLNK(head->ctl_table[index].mode);
 }
 
 static struct ctl_table_poll *sysctl_entry_poll(struct ctl_table_header *head,
 						size_t index)
 {
+	if (is_field_table(head))
+		return NULL;
+
 	return head->ctl_table[index].poll;
 }
 
+static void *sysctl_context_object(const struct sysctl_context *ctx)
+{
+	if (!ctx)
+		return NULL;
+	if (ctx->object)
+		return ctx->object(ctx);
+

fs/proc/proc_sysctl.c static int sysctl_follow_link(struct ctl_table_header **phead, size_t *pindex); · line 170

Quoted 10 lines
fs/proc/proc_sysctl.c +10 -0
+	switch (ctx->type) {
+	case SYSCTL_CONTEXT_USER_NS:
+		return ctx->ns.user_ns;
+	case SYSCTL_CONTEXT_IPC_NS:
+		return ctx->ns.ipc_ns;
+	case SYSCTL_CONTEXT_PID_NS:
+		return ctx->ns.pid_ns;
+	case SYSCTL_CONTEXT_NET_NS:
+		return ctx->ns.net_ns;
+	}

Does this mean that for every case where sysctl needs a context, you need a context type? Could you gather some of them together?

Quoted 78 lines
fs/proc/proc_sysctl.c +73 -1
+
+	return NULL;
+}
+
+static void *sysctl_context_data(const struct sysctl_context *ctx,
+				 size_t offset, size_t size)
+{
+	void *object = sysctl_context_object(ctx);
+
+	if (!object || offset > ctx->object_size ||
+	    size > ctx->object_size - offset)
+		return NULL;
+
+	return (char *)object + offset;
+}
+
 static const struct ctl_table *
 sysctl_entry_table(struct ctl_table_header *head, size_t index,
 		   struct ctl_table *table)
 {
-	return &head->ctl_table[index];
+	const struct sysctl_field *field;
+
+	if (!is_field_table(head))
+		return &head->ctl_table[index];
+
+	field = &head->ctl_fields[index];
+
+	memset(table, 0, sizeof(*table));
+	table->procname = field->procname;
+	table->mode = field->mode;
+
+	if (field->mode_fn)
+		table->mode = field->mode_fn(head->ctx);
+
+	switch (field->type) {
+	case SYSCTL_FIELD_NO_DATA:
+		break;
+	case SYSCTL_FIELD_STRING:
+		table->proc_handler = proc_dostring;
+		table->maxlen = field->maxlen;
+		break;
+	case SYSCTL_FIELD_BOOL:
+		table->proc_handler = proc_dobool;
+		table->maxlen = sizeof(bool);
+		break;
+	case SYSCTL_FIELD_U8:
+	case SYSCTL_FIELD_U8_MINMAX:
+		table->proc_handler = proc_dou8vec_minmax;
+		table->maxlen = sizeof(u8);
+		table->extra1 = field->u8_limits.min;
+		table->extra2 = field->u8_limits.max;
+		break;
+	case SYSCTL_FIELD_INT:
+	case SYSCTL_FIELD_INT_MINMAX:
+		table->proc_handler = field->type == SYSCTL_FIELD_INT ?
+				      proc_dointvec : proc_dointvec_minmax;
+		table->maxlen = sizeof(int);
+		table->extra1 = field->int_limits.min;
+		table->extra2 = field->int_limits.max;
+		break;
+	case SYSCTL_FIELD_UINT:
+	case SYSCTL_FIELD_UINT_MINMAX:
+		table->proc_handler = field->type == SYSCTL_FIELD_UINT ?
+				      proc_douintvec : proc_douintvec_minmax;
+		table->maxlen = sizeof(unsigned int);
+		table->extra1 = field->uint_limits.min;
+		table->extra2 = field->uint_limits.max;
+		break;
+	case SYSCTL_FIELD_LONG:
+	case SYSCTL_FIELD_LONG_MINMAX:
+		table->proc_handler = proc_doulongvec_minmax;
+		table->maxlen = sizeof(long);
+		table->extra1 = field->long_limits.min;
+		table->extra2 = field->long_limits.max;
+		break;
+	case SYSCTL_FIELD_ULONG:
+	case SYSCTL_FIELD_ULONG_MINMAX:

fs/proc/proc_sysctl.c static int sysctl_follow_link(struct ctl_table_header **phead, size_t *pindex); · line 257

Quoted 10 lines
fs/proc/proc_sysctl.c +10 -0
+		table->proc_handler = proc_doulongvec_minmax;
+		table->maxlen = sizeof(unsigned long);
+		table->extra1 = field->ulong_limits.min;
+		table->extra2 = field->ulong_limits.max;
+		break;
+	case SYSCTL_FIELD_SIZE_T:
+		table->proc_handler = proc_doulongvec_minmax;
+		table->maxlen = sizeof(size_t);
+		break;
+	}

Several things:
1. YOu replace all ctl_table members. What about the cases where you just need to replace one member because the others are the same for all the namespaces?

2. So if ctl_table grows another member, this function needs to be updated. right?

3. I get that the SYSCTL_FIELD_ULONG is a type. But the SYSCTL_FIELD_ULONG_MINMAX is not a type. "minmax" in sysctl usually means that we do a range check. Why is it treated like a type here?

Quoted 137 lines
fs/proc/proc_sysctl.c +47 -17
+
+	if (field->type != SYSCTL_FIELD_NO_DATA)
+		table->data = sysctl_context_data(head->ctx, field->data_offset, table->maxlen);
+	if (field->proc_handler)
+		table->proc_handler = field->proc_handler;
+	if (field->maxlen)
+		table->maxlen = field->maxlen;
+
+	return table;
 }
 
 static void sysctl_print_dir(struct ctl_dir *dir)
@@ -212,9 +363,17 @@ static void erase_entry(struct ctl_table_header *head, size_t index)
 
 static void init_header(struct ctl_table_header *head,
 	struct ctl_table_root *root, struct ctl_table_set *set,
-	struct ctl_node *node, const struct ctl_table *table, size_t table_size)
+	struct ctl_node *node, const struct ctl_table *table,
+	const struct sysctl_field *fields, size_t table_size,
+	const struct sysctl_context *ctx)
 {
-	head->ctl_table = table;
+	if (fields) {
+		head->ctl_fields = fields;
+		head->table_kind = SYSCTL_TABLE_KIND_FIELD;
+	} else {
+		head->ctl_table = table;
+		head->table_kind = SYSCTL_TABLE_KIND_TABLE;
+	}
 	head->ctl_table_size = table_size;
 	head->ctl_table_arg = table;
 	head->used = 0;
@@ -223,6 +382,7 @@ static void init_header(struct ctl_table_header *head,
 	head->unregistering = NULL;
 	head->root = root;
 	head->set = set;
+	head->ctx = ctx;
 	head->parent = NULL;
 	head->node = node;
 	INIT_HLIST_HEAD(&head->inodes);
@@ -981,7 +1141,7 @@ static struct ctl_dir *find_subdir(struct ctl_dir *dir,
 
 	if (!find_entry(&head, &index, dir, name, namelen))
 		return ERR_PTR(-ENOENT);
-	if (!S_ISDIR(sysctl_entry_mode(head, index)))
+	if (!sysctl_entry_is_dir(head, index))
 		return ERR_PTR(-ENOTDIR);
 	return container_of(head, struct ctl_dir, header);
 }
@@ -1006,7 +1166,8 @@ static struct ctl_dir *new_dir(struct ctl_table_set *set,
 	memcpy(new_name, name, namelen);
 	table[0].procname = new_name;
 	table[0].mode = S_IFDIR|S_IRUGO|S_IXUGO;
-	init_header(&new->header, set->dir.header.root, set, node, table, 1);
+	init_header(&new->header, set->dir.header.root, set, node, table, NULL,
+		    1, NULL);
 
 	return new;
 }
@@ -1187,6 +1348,10 @@ static int sysctl_check_table(const char *path, struct ctl_table_header *header)
 		entry = sysctl_entry_table(header, index, &table);
 		if (!entry->procname)
 			err |= sysctl_err(path, entry, "procname is null");
+		if (is_field_table(header) &&
+		    header->ctl_fields[index].type != SYSCTL_FIELD_NO_DATA &&
+		    !entry->data)
+			err |= sysctl_err(path, entry, "No data");
 		if ((entry->proc_handler == proc_dostring) ||
 		    (entry->proc_handler == proc_dobool) ||
 		    (entry->proc_handler == proc_dointvec) ||
@@ -1199,7 +1364,7 @@ static int sysctl_check_table(const char *path, struct ctl_table_header *header)
 		    (entry->proc_handler == proc_dointvec_ms_jiffies) ||
 		    (entry->proc_handler == proc_doulongvec_minmax) ||
 		    (entry->proc_handler == proc_doulongvec_ms_jiffies_minmax)) {
-			if (!entry->data)
+			if (!is_field_table(header) && !entry->data)
 				err |= sysctl_err(path, entry, "No data");
 			if (!entry->maxlen)
 				err |= sysctl_err(path, entry, "No maxlen");
@@ -1255,7 +1420,7 @@ static struct ctl_table_header *new_links(struct ctl_dir *dir, struct ctl_table_
 		link++;
 	}
 	init_header(links, dir->header.root, dir->header.set, node, link_table,
-		    head->ctl_table_size);
+		    NULL, head->ctl_table_size, NULL);
 	links->nreg = head->ctl_table_size;
 
 	return links;
@@ -1279,10 +1444,10 @@ static bool get_links(struct ctl_dir *dir,
 		if (!find_entry(&tmp_head, &link_index, dir, procname,
 				strlen(procname)))
 			return false;
-		if (S_ISDIR(sysctl_entry_mode(tmp_head, link_index)) &&
-		    S_ISDIR(sysctl_entry_mode(header, index)))
+		if (sysctl_entry_is_dir(tmp_head, link_index) &&
+		    sysctl_entry_is_dir(header, index))
 			continue;
-		if (S_ISLNK(sysctl_entry_mode(tmp_head, link_index)) &&
+		if (sysctl_entry_is_link(tmp_head, link_index) &&
 		    tmp_head->ctl_table[link_index].data == link_root)
 			continue;
 		return false;
@@ -1369,18 +1534,25 @@ static struct ctl_dir *sysctl_mkdir_p(struct ctl_dir *dir, const char *path)
 }
 
 /**
- * __register_sysctl_table - register a leaf sysctl table
+ * __register_sysctl_table_internal - register a leaf sysctl table
  * @set: Sysctl tree to register on
  * @path: The path to the directory the sysctl table is in.
+ * @table: The top-level ctl_table array, or %NULL when registering @fields.
+ * @fields: The top-level ctl_field array, or %NULL when registering @table.
+ * @table_size: The number of elements in @table or @fields.
+ * @ctx: Optional context used to resolve @fields entries.
+ * @ctx_size: Size of @ctx, including any wrapper object that embeds it.
  *
- * @table: the top-level table structure. This table should not be free'd
- *         after registration. So it should not be used on stack. It can either
- *         be a global or dynamically allocated by the caller and free'd later
- *         after sysctl unregistration.
- * @table_size : The number of elements in table
+ * Register a sysctl table hierarchy. One of @table or @fields must be
+ * provided. The descriptor array should not be freed after registration, so it
+ * should not be used on stack. It can either be global or dynamically
+ * allocated by the caller and freed later after sysctl unregistration.
  *
- * Register a sysctl table hierarchy. @table should be a filled in ctl_table
- * array.
+ * If @ctx points to a wrapper object, &struct sysctl_context must be the first
+ * member so @ctx can be copied together with the rest of that object.
+ * Data offsets stored in @fields are checked against @ctx->object_size and
+ * applied to the namespace selected by @ctx->ns. A context wrapper may provide
+ * @ctx->object for tables whose data belongs to another object.
  *
  * The members of the &struct ctl_table structure are used as follows:
  * procname - the name of the sysctl file under /proc/sys. Set to %NULL to not

fs/proc/proc_sysctl.c static struct ctl_dir *sysctl_mkdir_p(struct ctl_dir *dir, const char *path) · line 1589

Quoted 10 lines
fs/proc/proc_sysctl.c +4 -3
@@ -1411,25 +1583,69 @@ static struct ctl_dir *sysctl_mkdir_p(struct ctl_dir *dir, const char *path)
  * This routine returns %NULL on a failure to register, and a pointer
  * to the table header on success.
  */
-struct ctl_table_header *__register_sysctl_table(
-	struct ctl_table_set *set,
-	const char *path, const struct ctl_table *table, size_t table_size)
+static struct ctl_table_header *
+__register_sysctl_table_internal(struct ctl_table_set *set, const char *path,
+				 const struct ctl_table *table,
+				 const struct sysctl_field *fields,

It seems strange that these two args are getting passed to the same function, but cannot coexist. It is also strange that they hold very similar data.

It also makes me feel uneasy as we will have to think about two paths in the same function.

Quoted 214 lines
fs/proc/proc_sysctl.c +67 -5
+				 size_t table_size,
+				 const struct sysctl_context *ctx, size_t ctx_size)
 {
 	struct ctl_table_root *root = set->dir.header.root;
 	struct ctl_table_header *header;
 	struct ctl_dir *dir;
 	struct ctl_node *node;
+	const struct sysctl_context *header_ctx = NULL;
+	size_t nodes_size;
 	size_t alloc_size;
+	size_t context_offset;
+
+	if (!!table == !!fields)
+		return NULL;
+
+	if (ctx && ctx_size < sizeof(*ctx))
+		return NULL;
+
+	if (!ctx && ctx_size)
+		return NULL;
+
+	if (fields && (!ctx || !ctx->object_size ||
+		       !sysctl_context_object(ctx)))
+		return NULL;
+
+	if (check_mul_overflow(sizeof(struct ctl_node), table_size, &nodes_size))
+		return NULL;
+
+	if (check_add_overflow(sizeof(*header), nodes_size, &context_offset))
+		return NULL;
+
+	/*
+	 * Store the copied context after the ctl_node array. struct sysctl_context
+	 * is the first member of any caller-defined wrapper, whose alignment
+	 * must not exceed that of struct sysctl_context.
+	 */
+	if (ctx) {
+		if (check_add_overflow(context_offset,
+				       __alignof__(*ctx) - 1, &context_offset))
+			return NULL;
+		context_offset = ALIGN_DOWN(context_offset, __alignof__(*ctx));
+	}
 
-	alloc_size = sizeof(struct ctl_table_header) +
-		     sizeof(struct ctl_node) * table_size;
+	if (check_add_overflow(context_offset, ctx_size, &alloc_size))
+		return NULL;
 
 	header = kzalloc(alloc_size, GFP_KERNEL_ACCOUNT);
 	if (!header)
 		return NULL;
 
 	node = (struct ctl_node *)(header + 1);
-	init_header(header, root, set, node, table, table_size);
+	if (ctx) {
+		header_ctx = (const struct sysctl_context *)((void *)header +
+							   context_offset);
+		memcpy((void *)header_ctx, ctx, ctx_size);
+	}
+
+	init_header(header, root, set, node, table, fields, table_size,
+		    header_ctx);
 	if (sysctl_check_table(path, header))
 		goto fail;
 
@@ -1459,6 +1675,25 @@ struct ctl_table_header *__register_sysctl_table(
 	return NULL;
 }
 
+struct ctl_table_header *
+__register_sysctl_fields(struct ctl_table_set *set, const char *path,
+			 const struct sysctl_field *fields, size_t field_count,
+			 const struct sysctl_context *ctx, size_t ctx_size)
+{
+	return __register_sysctl_table_internal(set, path, NULL, fields,
+						field_count, ctx, ctx_size);
+}
+EXPORT_SYMBOL(__register_sysctl_fields);
+
+struct ctl_table_header *
+__register_sysctl_table(struct ctl_table_set *set, const char *path,
+			const struct ctl_table *table, size_t table_size)
+{
+	return __register_sysctl_table_internal(set, path, table, NULL,
+						table_size, NULL, 0);
+}
+EXPORT_SYMBOL(__register_sysctl_table);
+
 /**
  * register_sysctl_sz - register a sysctl table
  * @path: The path to the directory the sysctl table is in. If the path
@@ -1547,7 +1782,7 @@ static void put_links(struct ctl_table_header *header)
 
 		if (link &&
 		    ((S_ISDIR(link->mode) &&
-		      S_ISDIR(sysctl_entry_mode(header, index))) ||
+		      sysctl_entry_is_dir(header, index)) ||
 		     (S_ISLNK(link->mode) && (link->data == root)))) {
 			drop_sysctl_table(link_head);
 		} else {
@@ -1603,7 +1838,7 @@ void setup_sysctl_set(struct ctl_table_set *set,
 {
 	memset(set, 0, sizeof(*set));
 	set->is_seen = is_seen;
-	init_header(&set->dir.header, root, set, NULL, root_table, 1);
+	init_header(&set->dir.header, root, set, NULL, root_table, NULL, 1, NULL);
 }
 
 void retire_sysctl_set(struct ctl_table_set *set)
include/linux/sysctl.h +69 -0
index 7139a4c72736..de59fcd139cd 100644
@@ -22,20 +22,27 @@
 #ifndef _LINUX_SYSCTL_H
 #define _LINUX_SYSCTL_H
 
+#include <linux/build_bug.h>
 #include <linux/list.h>
 #include <linux/rcupdate.h>
 #include <linux/wait.h>
 #include <linux/rbtree.h>
+#include <linux/stddef.h>
 #include <linux/uidgid.h>
 #include <uapi/linux/sysctl.h>
 
 /* For the /proc/sys support */
 struct completion;
 struct ctl_table;
+struct sysctl_field;
 struct nsproxy;
 struct ctl_table_root;
 struct ctl_table_header;
 struct ctl_dir;
+struct ipc_namespace;
+struct net;
+struct pid_namespace;
+struct user_namespace;
 
 /* Keep the same order as in fs/proc/proc_sysctl.c */
 #define SYSCTL_ZERO			((void *)&sysctl_vals[0])
@@ -81,6 +88,27 @@ extern const int sysctl_vals[];
 extern const unsigned long sysctl_long_vals[];
 extern const unsigned int sysctl_uint_vals[];
 
+enum sysctl_context_type {
+	SYSCTL_CONTEXT_USER_NS,
+	SYSCTL_CONTEXT_IPC_NS,
+	SYSCTL_CONTEXT_PID_NS,
+	SYSCTL_CONTEXT_NET_NS,
+};
+
+union sysctl_namespace {
+	struct user_namespace *user_ns;
+	struct ipc_namespace *ipc_ns;
+	struct pid_namespace *pid_ns;
+	struct net *net_ns;
+};
+
+struct sysctl_context {
+	enum sysctl_context_type type;
+	size_t object_size;
+	union sysctl_namespace ns;
+	void *(*object)(const struct sysctl_context *ctx);
+};
+
 typedef int proc_handler(const struct ctl_table *ctl, int dir, void *buf,
 			 size_t *lenp, loff_t *ppos);
 
@@ -237,30 +265,114 @@ struct ctl_table {
 	void *extra2;
 } __randomize_layout;
 
+enum sysctl_field_type {
+	SYSCTL_FIELD_NO_DATA,
+	SYSCTL_FIELD_STRING,
+	SYSCTL_FIELD_BOOL,
+	SYSCTL_FIELD_U8,
+	SYSCTL_FIELD_U8_MINMAX,
+	SYSCTL_FIELD_INT,
+	SYSCTL_FIELD_INT_MINMAX,
+	SYSCTL_FIELD_UINT,
+	SYSCTL_FIELD_UINT_MINMAX,
+	SYSCTL_FIELD_LONG,
+	SYSCTL_FIELD_LONG_MINMAX,
+	SYSCTL_FIELD_ULONG,
+	SYSCTL_FIELD_ULONG_MINMAX,
+	SYSCTL_FIELD_SIZE_T,
+};
+
+#define __SYSCTL_FIELD_OFFSET(_struct, _field, _type)				\
+	(offsetof(_struct, _field) +						\
+	 BUILD_BUG_ON_ZERO(!__same_type(((_struct *)0)->_field, *(_type *)0)))
+
+#define SYSCTL_FIELD_INT_OFFSET(_struct, _field)	__SYSCTL_FIELD_OFFSET(_struct, _field, int)
+#define SYSCTL_FIELD_UINT_OFFSET(_struct, _field)	__SYSCTL_FIELD_OFFSET(_struct, _field, unsigned int)
+#define SYSCTL_FIELD_LONG_OFFSET(_struct, _field)	__SYSCTL_FIELD_OFFSET(_struct, _field, long)
+#define SYSCTL_FIELD_ULONG_OFFSET(_struct, _field)	__SYSCTL_FIELD_OFFSET(_struct, _field, unsigned long)
+#define SYSCTL_FIELD_SIZE_T_OFFSET(_struct, _field)	__SYSCTL_FIELD_OFFSET(_struct, _field, size_t)
+
+struct sysctl_field_u8_limits {
+	unsigned int *min;
+	unsigned int *max;
+};
+
+struct sysctl_field_int_limits {
+	int *min;
+	int *max;
+};
+
+struct sysctl_field_uint_limits {
+	unsigned int *min;
+	unsigned int *max;
+};

include/linux/sysctl.h struct ctl_table { · line 318

Quoted 10 lines
include/linux/sysctl.h +10 -0
+
+struct sysctl_field_long_limits {
+	long *min;
+	long *max;
+};
+
+struct sysctl_field_ulong_limits {
+	unsigned long *min;
+	unsigned long *max;
+};

These min,max values have been an ongoing issue. Here we just propagate them further.

Quoted 121 lines
include/linux/sysctl.h +59 -5
+
+struct sysctl_field {
+	const char *procname;
+	umode_t mode;
+	enum sysctl_field_type type;
+	umode_t (*mode_fn)(const struct sysctl_context *ctx);
+	proc_handler *proc_handler;
+	int maxlen;
+	size_t data_offset;
+	union {
+		struct sysctl_field_u8_limits		u8_limits;
+		struct sysctl_field_int_limits		int_limits;
+		struct sysctl_field_uint_limits		uint_limits;
+		struct sysctl_field_long_limits		long_limits;
+		struct sysctl_field_ulong_limits	ulong_limits;
+	};
+} __randomize_layout;
+
 struct ctl_node {
 	struct rb_node node;
 	struct ctl_table_header *header;
 };
 
 /**
- * struct ctl_table_header - maintains dynamic lists of struct ctl_table trees
- * @ctl_table: pointer to the first element in ctl_table array
- * @ctl_table_size: number of elements pointed by @ctl_table
+ * struct ctl_table_header - maintains dynamic lists of sysctl descriptor trees
+ * @ctl_table: pointer to the first element in a legacy ctl_table array
+ * @ctl_fields: pointer to the first element in a ctl_field array
+ * @ctl_table_size: number of elements pointed to by @ctl_table or @ctl_fields
  * @used: The entry will never be touched when equal to 0.
  * @count: Upped every time something is added to @inodes and downed every time
  *         something is removed from inodes
  * @nreg: When nreg drops to 0 the ctl_table_header will be unregistered.
- * @rcu: Delays the freeing of the inode. Introduced with "unfuck proc_sysctl ->d_compare()"
+ * @rcu: delays freeing the header until after an RCU grace period
+ * @unregistering: completion used while unregistering the header
+ * @ctl_table_arg: original legacy ctl_table passed at registration, or NULL
+ * @ctx: copied registration context used to resolve ctl_field entries
+ * @root: sysctl tree containing this header
+ * @set: sysctl set containing this header
+ * @parent: parent directory of this header
+ * @node: array of nodes corresponding to the descriptor entries
+ * @inodes: inodes currently referring to this header
  *
  * @type: Enumeration to differentiate between ctl target types:
  * type.SYSCTL_TABLE_TYPE_DEFAULT: ctl target with no special considerations
  * type.SYSCTL_TABLE_TYPE_PERMANENTLY_EMPTY: Identifies a permanently empty dir
  *                                            target to serve as a mount point
+ * @table_kind: descriptor format stored in this header
+ * @table_kind.SYSCTL_TABLE_KIND_TABLE: legacy ctl_table descriptors
+ * @table_kind.SYSCTL_TABLE_KIND_FIELD: typed ctl_field descriptors
  */
 struct ctl_table_header {
 	union {
 		struct {
-			const struct ctl_table *ctl_table;
+			union {
+				const struct ctl_table *ctl_table;
+				const struct sysctl_field *ctl_fields;
+			};
 			int ctl_table_size;
 			int used;
 			int count;
@@ -275,10 +387,15 @@ struct ctl_table_header {
 	struct ctl_dir *parent;
 	struct ctl_node *node;
 	struct hlist_head inodes; /* head for proc_inode->sysctl_inodes */
+	const struct sysctl_context *ctx;
 	enum {
 		SYSCTL_TABLE_TYPE_DEFAULT,
 		SYSCTL_TABLE_TYPE_PERMANENTLY_EMPTY,
 	} type;
+	enum {
+		SYSCTL_TABLE_KIND_TABLE,
+		SYSCTL_TABLE_KIND_FIELD,
+	} table_kind;
 };
 
 struct ctl_dir {
@@ -303,6 +420,10 @@ struct ctl_table_root {
 #define register_sysctl(path, table)	\
 	register_sysctl_sz(path, table, ARRAY_SIZE(table))
 
+#define register_sysctl_fields(set, path, fields, ctx)			\
+	__register_sysctl_fields(set, path, fields, ARRAY_SIZE(fields),	\
+				 (ctx), sizeof(*(ctx)))
+
 #ifdef CONFIG_SYSCTL
 
 void proc_sys_poll_notify(struct ctl_table_poll *poll);
@@ -315,6 +436,10 @@ extern void retire_sysctl_set(struct ctl_table_set *set);
 struct ctl_table_header *__register_sysctl_table(
 	struct ctl_table_set *set,
 	const char *path, const struct ctl_table *table, size_t table_size);
+struct ctl_table_header *
+__register_sysctl_fields(struct ctl_table_set *set, const char *path,
+			 const struct sysctl_field *fields, size_t field_count,
+			 const struct sysctl_context *ctx, size_t ctx_size);
 struct ctl_table_header *register_sysctl_sz(const char *path, const struct ctl_table *table,
 					    size_t table_size);
 void unregister_sysctl_table(struct ctl_table_header * table);
@@ -350,6 +475,14 @@ static inline struct ctl_table_header *register_sysctl_sz(const char *path,
 	return NULL;
 }
 
+static inline struct ctl_table_header *
+__register_sysctl_fields(struct ctl_table_set *set, const char *path,
+			 const struct sysctl_field *fields, size_t field_count,
+			 const struct sysctl_context *ctx, size_t ctx_size)
+{
+	return NULL;
+}
+
 static inline void unregister_sysctl_table(struct ctl_table_header * table)
 {
 }
Signature · 1 line
2.55.0
#7 lore raw
Quoted 111 lines · Joel Granados
Quoted 110 lines · Alexey Gladkov

Several sysctl users duplicate ctl_table arrays at registration time so data and limit pointers can be redirected to namespace or device state. The copies consume memory for every instance and their index-based fixups silently depend on the source table order.

Add sysctl_field as an alternative static descriptor. A field records the value kind and a checked offset into an object selected by a registration context. Type-specific offset helpers verify the backing member type at build time, while the core derives the legacy proc handler, size and limit pointers from the field kind.

Keep ctl_table as the interface used by proc handlers, permissions and BPF by materializing one entry on the stack when those paths need it. Existing ctl_table registrations are unchanged, and converted users can share one read-only descriptor array without allocating a table copy.

---
 fs/proc/proc_sysctl.c  | 289 +++++++++++++++++++++++++++++++++++++----
 include/linux/sysctl.h | 143 +++++++++++++++++++-
 2 files changed, 400 insertions(+), 32 deletions(-)
fs/proc/proc_sysctl.c +56 -1
index b5cb219bcdbc..73f3408b2043 100644
@@ -18,6 +18,7 @@
 #include <linux/mount.h>
 #include <linux/kmemleak.h>
 #include <linux/lockdep.h>
+#include <linux/overflow.h>
 #include "internal.h"
 
 #define list_for_each_table_entry(index, header)			\
@@ -91,28 +92,178 @@ static int sysctl_follow_link(struct ctl_table_header **phead, size_t *pindex);
 static int insert_links(struct ctl_table_header *head);
 static void put_links(struct ctl_table_header *header);
 
+static inline bool is_field_table(const struct ctl_table_header *head)
+{
+	return head->table_kind == SYSCTL_TABLE_KIND_FIELD;
+}
+
 static const char *sysctl_entry_procname(struct ctl_table_header *head,
 					 size_t index)
 {
+	if (is_field_table(head))
+		return head->ctl_fields[index].procname;
+
 	return head->ctl_table[index].procname;
 }
 
 static umode_t sysctl_entry_mode(struct ctl_table_header *head, size_t index)
 {
-	return head->ctl_table[index].mode;
+	const struct sysctl_field *field;
+
+	if (!is_field_table(head))
+		return head->ctl_table[index].mode;
+
+	field = &head->ctl_fields[index];
+
+	if (field->mode_fn) {
+		lockdep_assert_not_held(&sysctl_lock);
+		return field->mode_fn(head->ctx);
+	}
+
+	return field->mode;
+}
+
+static bool sysctl_entry_is_dir(const struct ctl_table_header *head,
+				size_t index)
+{
+	/* Do not call field mode_fn callback while holding sysctl_lock. */
+	if (is_field_table(head))
+		return false;
+
+	return S_ISDIR(head->ctl_table[index].mode);
+}
+
+static bool sysctl_entry_is_link(const struct ctl_table_header *head,
+				 size_t index)
+{
+	if (is_field_table(head))
+		return false;
+
+	return S_ISLNK(head->ctl_table[index].mode);
 }
 
 static struct ctl_table_poll *sysctl_entry_poll(struct ctl_table_header *head,
 						size_t index)
 {
+	if (is_field_table(head))
+		return NULL;
+
 	return head->ctl_table[index].poll;
 }
 
+static void *sysctl_context_object(const struct sysctl_context *ctx)
+{
+	if (!ctx)
+		return NULL;
+	if (ctx->object)
+		return ctx->object(ctx);
+
+	switch (ctx->type) {
+	case SYSCTL_CONTEXT_USER_NS:
+		return ctx->ns.user_ns;
+	case SYSCTL_CONTEXT_IPC_NS:
+		return ctx->ns.ipc_ns;
Quoted 8 lines

fs/proc/proc_sysctl.c static int sysctl_follow_link(struct ctl_table_header **phead, size_t *pindex); · line 170

Quoted 5 lines
fs/proc/proc_sysctl.c +5 -0
+	case SYSCTL_CONTEXT_PID_NS:
+		return ctx->ns.pid_ns;
+	case SYSCTL_CONTEXT_NET_NS:
+		return ctx->ns.net_ns;
+	}

Does this mean that for every case where sysctl needs a context, you need a context type? Could you gather some of them together?

These are literally all the types of namespaces currently in use that need to be distinguished. A few special cases (actually, just one or two) can be handled without expanding the context.

Quoted 85 lines
Quoted 85 lines
fs/proc/proc_sysctl.c +80 -1
+
+	return NULL;
+}
+
+static void *sysctl_context_data(const struct sysctl_context *ctx,
+				 size_t offset, size_t size)
+{
+	void *object = sysctl_context_object(ctx);
+
+	if (!object || offset > ctx->object_size ||
+	    size > ctx->object_size - offset)
+		return NULL;
+
+	return (char *)object + offset;
+}
+
 static const struct ctl_table *
 sysctl_entry_table(struct ctl_table_header *head, size_t index,
 		   struct ctl_table *table)
 {
-	return &head->ctl_table[index];
+	const struct sysctl_field *field;
+
+	if (!is_field_table(head))
+		return &head->ctl_table[index];
+
+	field = &head->ctl_fields[index];
+
+	memset(table, 0, sizeof(*table));
+	table->procname = field->procname;
+	table->mode = field->mode;
+
+	if (field->mode_fn)
+		table->mode = field->mode_fn(head->ctx);
+
+	switch (field->type) {
+	case SYSCTL_FIELD_NO_DATA:
+		break;
+	case SYSCTL_FIELD_STRING:
+		table->proc_handler = proc_dostring;
+		table->maxlen = field->maxlen;
+		break;
+	case SYSCTL_FIELD_BOOL:
+		table->proc_handler = proc_dobool;
+		table->maxlen = sizeof(bool);
+		break;
+	case SYSCTL_FIELD_U8:
+	case SYSCTL_FIELD_U8_MINMAX:
+		table->proc_handler = proc_dou8vec_minmax;
+		table->maxlen = sizeof(u8);
+		table->extra1 = field->u8_limits.min;
+		table->extra2 = field->u8_limits.max;
+		break;
+	case SYSCTL_FIELD_INT:
+	case SYSCTL_FIELD_INT_MINMAX:
+		table->proc_handler = field->type == SYSCTL_FIELD_INT ?
+				      proc_dointvec : proc_dointvec_minmax;
+		table->maxlen = sizeof(int);
+		table->extra1 = field->int_limits.min;
+		table->extra2 = field->int_limits.max;
+		break;
+	case SYSCTL_FIELD_UINT:
+	case SYSCTL_FIELD_UINT_MINMAX:
+		table->proc_handler = field->type == SYSCTL_FIELD_UINT ?
+				      proc_douintvec : proc_douintvec_minmax;
+		table->maxlen = sizeof(unsigned int);
+		table->extra1 = field->uint_limits.min;
+		table->extra2 = field->uint_limits.max;
+		break;
+	case SYSCTL_FIELD_LONG:
+	case SYSCTL_FIELD_LONG_MINMAX:
+		table->proc_handler = proc_doulongvec_minmax;
+		table->maxlen = sizeof(long);
+		table->extra1 = field->long_limits.min;
+		table->extra2 = field->long_limits.max;
+		break;
+	case SYSCTL_FIELD_ULONG:
+	case SYSCTL_FIELD_ULONG_MINMAX:
+		table->proc_handler = proc_doulongvec_minmax;
+		table->maxlen = sizeof(unsigned long);
+		table->extra1 = field->ulong_limits.min;
+		table->extra2 = field->ulong_limits.max;
+		break;
+	case SYSCTL_FIELD_SIZE_T:
+		table->proc_handler = proc_doulongvec_minmax;
Quoted 8 lines

fs/proc/proc_sysctl.c static int sysctl_follow_link(struct ctl_table_header **phead, size_t *pindex); · line 257

Quoted 3 lines
fs/proc/proc_sysctl.c +3 -0
+		table->maxlen = sizeof(size_t);
+		break;
+	}

Several things:
1. YOu replace all ctl_table members. What about the cases where you just need to replace one member because the others are the same for all the namespaces?

This is a simple function for creating a ctl_table from a sysctl_field. I wanted to keep it simple and easy to read since it operates on a per-table-entry basis.

I can add more comparisons, but that would be a micro-optimization since the values of some fields are enforced for a given type (proc_handler, maxlen, extra); others can be dynamic (mode, data).

Quoted 2 lines

2. So if ctl_table grows another member, this function needs to be updated. right?

Yes, because this struct is used for legacy code. Ok, not legacy code, but the rest of the current code.

Quoted 3 lines

3. I get that the SYSCTL_FIELD_ULONG is a type. But the SYSCTL_FIELD_ULONG_MINMAX is not a type. "minmax" in sysctl usually means that we do a range check. Why is it treated like a type here?

Yes, U8_MINMAX, LONG_MINMAX, and ULONG_MINMAX can be removed. I'd be happy to get rid of them altogether. I just didn't want them to differ from INT_MINMAX and UINT_MINMAX.

Quoted 152 lines
Quoted 137 lines
fs/proc/proc_sysctl.c +47 -17
+
+	if (field->type != SYSCTL_FIELD_NO_DATA)
+		table->data = sysctl_context_data(head->ctx, field->data_offset, table->maxlen);
+	if (field->proc_handler)
+		table->proc_handler = field->proc_handler;
+	if (field->maxlen)
+		table->maxlen = field->maxlen;
+
+	return table;
 }
 
 static void sysctl_print_dir(struct ctl_dir *dir)
@@ -212,9 +363,17 @@ static void erase_entry(struct ctl_table_header *head, size_t index)
 
 static void init_header(struct ctl_table_header *head,
 	struct ctl_table_root *root, struct ctl_table_set *set,
-	struct ctl_node *node, const struct ctl_table *table, size_t table_size)
+	struct ctl_node *node, const struct ctl_table *table,
+	const struct sysctl_field *fields, size_t table_size,
+	const struct sysctl_context *ctx)
 {
-	head->ctl_table = table;
+	if (fields) {
+		head->ctl_fields = fields;
+		head->table_kind = SYSCTL_TABLE_KIND_FIELD;
+	} else {
+		head->ctl_table = table;
+		head->table_kind = SYSCTL_TABLE_KIND_TABLE;
+	}
 	head->ctl_table_size = table_size;
 	head->ctl_table_arg = table;
 	head->used = 0;
@@ -223,6 +382,7 @@ static void init_header(struct ctl_table_header *head,
 	head->unregistering = NULL;
 	head->root = root;
 	head->set = set;
+	head->ctx = ctx;
 	head->parent = NULL;
 	head->node = node;
 	INIT_HLIST_HEAD(&head->inodes);
@@ -981,7 +1141,7 @@ static struct ctl_dir *find_subdir(struct ctl_dir *dir,
 
 	if (!find_entry(&head, &index, dir, name, namelen))
 		return ERR_PTR(-ENOENT);
-	if (!S_ISDIR(sysctl_entry_mode(head, index)))
+	if (!sysctl_entry_is_dir(head, index))
 		return ERR_PTR(-ENOTDIR);
 	return container_of(head, struct ctl_dir, header);
 }
@@ -1006,7 +1166,8 @@ static struct ctl_dir *new_dir(struct ctl_table_set *set,
 	memcpy(new_name, name, namelen);
 	table[0].procname = new_name;
 	table[0].mode = S_IFDIR|S_IRUGO|S_IXUGO;
-	init_header(&new->header, set->dir.header.root, set, node, table, 1);
+	init_header(&new->header, set->dir.header.root, set, node, table, NULL,
+		    1, NULL);
 
 	return new;
 }
@@ -1187,6 +1348,10 @@ static int sysctl_check_table(const char *path, struct ctl_table_header *header)
 		entry = sysctl_entry_table(header, index, &table);
 		if (!entry->procname)
 			err |= sysctl_err(path, entry, "procname is null");
+		if (is_field_table(header) &&
+		    header->ctl_fields[index].type != SYSCTL_FIELD_NO_DATA &&
+		    !entry->data)
+			err |= sysctl_err(path, entry, "No data");
 		if ((entry->proc_handler == proc_dostring) ||
 		    (entry->proc_handler == proc_dobool) ||
 		    (entry->proc_handler == proc_dointvec) ||
@@ -1199,7 +1364,7 @@ static int sysctl_check_table(const char *path, struct ctl_table_header *header)
 		    (entry->proc_handler == proc_dointvec_ms_jiffies) ||
 		    (entry->proc_handler == proc_doulongvec_minmax) ||
 		    (entry->proc_handler == proc_doulongvec_ms_jiffies_minmax)) {
-			if (!entry->data)
+			if (!is_field_table(header) && !entry->data)
 				err |= sysctl_err(path, entry, "No data");
 			if (!entry->maxlen)
 				err |= sysctl_err(path, entry, "No maxlen");
@@ -1255,7 +1420,7 @@ static struct ctl_table_header *new_links(struct ctl_dir *dir, struct ctl_table_
 		link++;
 	}
 	init_header(links, dir->header.root, dir->header.set, node, link_table,
-		    head->ctl_table_size);
+		    NULL, head->ctl_table_size, NULL);
 	links->nreg = head->ctl_table_size;
 
 	return links;
@@ -1279,10 +1444,10 @@ static bool get_links(struct ctl_dir *dir,
 		if (!find_entry(&tmp_head, &link_index, dir, procname,
 				strlen(procname)))
 			return false;
-		if (S_ISDIR(sysctl_entry_mode(tmp_head, link_index)) &&
-		    S_ISDIR(sysctl_entry_mode(header, index)))
+		if (sysctl_entry_is_dir(tmp_head, link_index) &&
+		    sysctl_entry_is_dir(header, index))
 			continue;
-		if (S_ISLNK(sysctl_entry_mode(tmp_head, link_index)) &&
+		if (sysctl_entry_is_link(tmp_head, link_index) &&
 		    tmp_head->ctl_table[link_index].data == link_root)
 			continue;
 		return false;
@@ -1369,18 +1534,25 @@ static struct ctl_dir *sysctl_mkdir_p(struct ctl_dir *dir, const char *path)
 }
 
 /**
- * __register_sysctl_table - register a leaf sysctl table
+ * __register_sysctl_table_internal - register a leaf sysctl table
  * @set: Sysctl tree to register on
  * @path: The path to the directory the sysctl table is in.
+ * @table: The top-level ctl_table array, or %NULL when registering @fields.
+ * @fields: The top-level ctl_field array, or %NULL when registering @table.
+ * @table_size: The number of elements in @table or @fields.
+ * @ctx: Optional context used to resolve @fields entries.
+ * @ctx_size: Size of @ctx, including any wrapper object that embeds it.
  *
- * @table: the top-level table structure. This table should not be free'd
- *         after registration. So it should not be used on stack. It can either
- *         be a global or dynamically allocated by the caller and free'd later
- *         after sysctl unregistration.
- * @table_size : The number of elements in table
+ * Register a sysctl table hierarchy. One of @table or @fields must be
+ * provided. The descriptor array should not be freed after registration, so it
+ * should not be used on stack. It can either be global or dynamically
+ * allocated by the caller and freed later after sysctl unregistration.
  *
- * Register a sysctl table hierarchy. @table should be a filled in ctl_table
- * array.
+ * If @ctx points to a wrapper object, &struct sysctl_context must be the first
+ * member so @ctx can be copied together with the rest of that object.
+ * Data offsets stored in @fields are checked against @ctx->object_size and
+ * applied to the namespace selected by @ctx->ns. A context wrapper may provide
+ * @ctx->object for tables whose data belongs to another object.
  *
  * The members of the &struct ctl_table structure are used as follows:
  * procname - the name of the sysctl file under /proc/sys. Set to %NULL to not

fs/proc/proc_sysctl.c static struct ctl_dir *sysctl_mkdir_p(struct ctl_dir *dir, const char *path) · line 1589

Quoted 10 lines
fs/proc/proc_sysctl.c +4 -3
@@ -1411,25 +1583,69 @@ static struct ctl_dir *sysctl_mkdir_p(struct ctl_dir *dir, const char *path)
  * This routine returns %NULL on a failure to register, and a pointer
  * to the table header on success.
  */
-struct ctl_table_header *__register_sysctl_table(
-	struct ctl_table_set *set,
-	const char *path, const struct ctl_table *table, size_t table_size)
+static struct ctl_table_header *
+__register_sysctl_table_internal(struct ctl_table_set *set, const char *path,
+				 const struct ctl_table *table,
+				 const struct sysctl_field *fields,

It seems strange that these two args are getting passed to the same function, but cannot coexist. It is also strange that they hold very similar data.

Quoted 2 lines

It also makes me feel uneasy as we will have to think about two paths in the same function.

Do you want me to separate them?

They're very similar in all other respects, but you're right - there's a mix of two paths here.

Quoted 219 lines
Quoted 219 lines
fs/proc/proc_sysctl.c +67 -5
+				 size_t table_size,
+				 const struct sysctl_context *ctx, size_t ctx_size)
 {
 	struct ctl_table_root *root = set->dir.header.root;
 	struct ctl_table_header *header;
 	struct ctl_dir *dir;
 	struct ctl_node *node;
+	const struct sysctl_context *header_ctx = NULL;
+	size_t nodes_size;
 	size_t alloc_size;
+	size_t context_offset;
+
+	if (!!table == !!fields)
+		return NULL;
+
+	if (ctx && ctx_size < sizeof(*ctx))
+		return NULL;
+
+	if (!ctx && ctx_size)
+		return NULL;
+
+	if (fields && (!ctx || !ctx->object_size ||
+		       !sysctl_context_object(ctx)))
+		return NULL;
+
+	if (check_mul_overflow(sizeof(struct ctl_node), table_size, &nodes_size))
+		return NULL;
+
+	if (check_add_overflow(sizeof(*header), nodes_size, &context_offset))
+		return NULL;
+
+	/*
+	 * Store the copied context after the ctl_node array. struct sysctl_context
+	 * is the first member of any caller-defined wrapper, whose alignment
+	 * must not exceed that of struct sysctl_context.
+	 */
+	if (ctx) {
+		if (check_add_overflow(context_offset,
+				       __alignof__(*ctx) - 1, &context_offset))
+			return NULL;
+		context_offset = ALIGN_DOWN(context_offset, __alignof__(*ctx));
+	}
 
-	alloc_size = sizeof(struct ctl_table_header) +
-		     sizeof(struct ctl_node) * table_size;
+	if (check_add_overflow(context_offset, ctx_size, &alloc_size))
+		return NULL;
 
 	header = kzalloc(alloc_size, GFP_KERNEL_ACCOUNT);
 	if (!header)
 		return NULL;
 
 	node = (struct ctl_node *)(header + 1);
-	init_header(header, root, set, node, table, table_size);
+	if (ctx) {
+		header_ctx = (const struct sysctl_context *)((void *)header +
+							   context_offset);
+		memcpy((void *)header_ctx, ctx, ctx_size);
+	}
+
+	init_header(header, root, set, node, table, fields, table_size,
+		    header_ctx);
 	if (sysctl_check_table(path, header))
 		goto fail;
 
@@ -1459,6 +1675,25 @@ struct ctl_table_header *__register_sysctl_table(
 	return NULL;
 }
 
+struct ctl_table_header *
+__register_sysctl_fields(struct ctl_table_set *set, const char *path,
+			 const struct sysctl_field *fields, size_t field_count,
+			 const struct sysctl_context *ctx, size_t ctx_size)
+{
+	return __register_sysctl_table_internal(set, path, NULL, fields,
+						field_count, ctx, ctx_size);
+}
+EXPORT_SYMBOL(__register_sysctl_fields);
+
+struct ctl_table_header *
+__register_sysctl_table(struct ctl_table_set *set, const char *path,
+			const struct ctl_table *table, size_t table_size)
+{
+	return __register_sysctl_table_internal(set, path, table, NULL,
+						table_size, NULL, 0);
+}
+EXPORT_SYMBOL(__register_sysctl_table);
+
 /**
  * register_sysctl_sz - register a sysctl table
  * @path: The path to the directory the sysctl table is in. If the path
@@ -1547,7 +1782,7 @@ static void put_links(struct ctl_table_header *header)
 
 		if (link &&
 		    ((S_ISDIR(link->mode) &&
-		      S_ISDIR(sysctl_entry_mode(header, index))) ||
+		      sysctl_entry_is_dir(header, index)) ||
 		     (S_ISLNK(link->mode) && (link->data == root)))) {
 			drop_sysctl_table(link_head);
 		} else {
@@ -1603,7 +1838,7 @@ void setup_sysctl_set(struct ctl_table_set *set,
 {
 	memset(set, 0, sizeof(*set));
 	set->is_seen = is_seen;
-	init_header(&set->dir.header, root, set, NULL, root_table, 1);
+	init_header(&set->dir.header, root, set, NULL, root_table, NULL, 1, NULL);
 }
 
 void retire_sysctl_set(struct ctl_table_set *set)
include/linux/sysctl.h +74 -0
index 7139a4c72736..de59fcd139cd 100644
@@ -22,20 +22,27 @@
 #ifndef _LINUX_SYSCTL_H
 #define _LINUX_SYSCTL_H
 
+#include <linux/build_bug.h>
 #include <linux/list.h>
 #include <linux/rcupdate.h>
 #include <linux/wait.h>
 #include <linux/rbtree.h>
+#include <linux/stddef.h>
 #include <linux/uidgid.h>
 #include <uapi/linux/sysctl.h>
 
 /* For the /proc/sys support */
 struct completion;
 struct ctl_table;
+struct sysctl_field;
 struct nsproxy;
 struct ctl_table_root;
 struct ctl_table_header;
 struct ctl_dir;
+struct ipc_namespace;
+struct net;
+struct pid_namespace;
+struct user_namespace;
 
 /* Keep the same order as in fs/proc/proc_sysctl.c */
 #define SYSCTL_ZERO			((void *)&sysctl_vals[0])
@@ -81,6 +88,27 @@ extern const int sysctl_vals[];
 extern const unsigned long sysctl_long_vals[];
 extern const unsigned int sysctl_uint_vals[];
 
+enum sysctl_context_type {
+	SYSCTL_CONTEXT_USER_NS,
+	SYSCTL_CONTEXT_IPC_NS,
+	SYSCTL_CONTEXT_PID_NS,
+	SYSCTL_CONTEXT_NET_NS,
+};
+
+union sysctl_namespace {
+	struct user_namespace *user_ns;
+	struct ipc_namespace *ipc_ns;
+	struct pid_namespace *pid_ns;
+	struct net *net_ns;
+};
+
+struct sysctl_context {
+	enum sysctl_context_type type;
+	size_t object_size;
+	union sysctl_namespace ns;
+	void *(*object)(const struct sysctl_context *ctx);
+};
+
 typedef int proc_handler(const struct ctl_table *ctl, int dir, void *buf,
 			 size_t *lenp, loff_t *ppos);
 
@@ -237,30 +265,114 @@ struct ctl_table {
 	void *extra2;
 } __randomize_layout;
 
+enum sysctl_field_type {
+	SYSCTL_FIELD_NO_DATA,
+	SYSCTL_FIELD_STRING,
+	SYSCTL_FIELD_BOOL,
+	SYSCTL_FIELD_U8,
+	SYSCTL_FIELD_U8_MINMAX,
+	SYSCTL_FIELD_INT,
+	SYSCTL_FIELD_INT_MINMAX,
+	SYSCTL_FIELD_UINT,
+	SYSCTL_FIELD_UINT_MINMAX,
+	SYSCTL_FIELD_LONG,
+	SYSCTL_FIELD_LONG_MINMAX,
+	SYSCTL_FIELD_ULONG,
+	SYSCTL_FIELD_ULONG_MINMAX,
+	SYSCTL_FIELD_SIZE_T,
+};
+
+#define __SYSCTL_FIELD_OFFSET(_struct, _field, _type)				\
+	(offsetof(_struct, _field) +						\
+	 BUILD_BUG_ON_ZERO(!__same_type(((_struct *)0)->_field, *(_type *)0)))
+
+#define SYSCTL_FIELD_INT_OFFSET(_struct, _field)	__SYSCTL_FIELD_OFFSET(_struct, _field, int)
+#define SYSCTL_FIELD_UINT_OFFSET(_struct, _field)	__SYSCTL_FIELD_OFFSET(_struct, _field, unsigned int)
+#define SYSCTL_FIELD_LONG_OFFSET(_struct, _field)	__SYSCTL_FIELD_OFFSET(_struct, _field, long)
+#define SYSCTL_FIELD_ULONG_OFFSET(_struct, _field)	__SYSCTL_FIELD_OFFSET(_struct, _field, unsigned long)
+#define SYSCTL_FIELD_SIZE_T_OFFSET(_struct, _field)	__SYSCTL_FIELD_OFFSET(_struct, _field, size_t)
+
+struct sysctl_field_u8_limits {
+	unsigned int *min;
+	unsigned int *max;
+};
+
+struct sysctl_field_int_limits {
+	int *min;
+	int *max;
+};
+
+struct sysctl_field_uint_limits {
+	unsigned int *min;
+	unsigned int *max;
+};
+
+struct sysctl_field_long_limits {
+	long *min;
+	long *max;
+};
Quoted 8 lines

include/linux/sysctl.h struct ctl_table { · line 318

Quoted 5 lines
include/linux/sysctl.h +5 -0
+
+struct sysctl_field_ulong_limits {
+	unsigned long *min;
+	unsigned long *max;
+};

These min,max values have been an ongoing issue. Here we just propagate them further.

Do you mean min,max in general?

Quoted 121 lines
Quoted 121 lines
include/linux/sysctl.h +59 -5
+
+struct sysctl_field {
+	const char *procname;
+	umode_t mode;
+	enum sysctl_field_type type;
+	umode_t (*mode_fn)(const struct sysctl_context *ctx);
+	proc_handler *proc_handler;
+	int maxlen;
+	size_t data_offset;
+	union {
+		struct sysctl_field_u8_limits		u8_limits;
+		struct sysctl_field_int_limits		int_limits;
+		struct sysctl_field_uint_limits		uint_limits;
+		struct sysctl_field_long_limits		long_limits;
+		struct sysctl_field_ulong_limits	ulong_limits;
+	};
+} __randomize_layout;
+
 struct ctl_node {
 	struct rb_node node;
 	struct ctl_table_header *header;
 };
 
 /**
- * struct ctl_table_header - maintains dynamic lists of struct ctl_table trees
- * @ctl_table: pointer to the first element in ctl_table array
- * @ctl_table_size: number of elements pointed by @ctl_table
+ * struct ctl_table_header - maintains dynamic lists of sysctl descriptor trees
+ * @ctl_table: pointer to the first element in a legacy ctl_table array
+ * @ctl_fields: pointer to the first element in a ctl_field array
+ * @ctl_table_size: number of elements pointed to by @ctl_table or @ctl_fields
  * @used: The entry will never be touched when equal to 0.
  * @count: Upped every time something is added to @inodes and downed every time
  *         something is removed from inodes
  * @nreg: When nreg drops to 0 the ctl_table_header will be unregistered.
- * @rcu: Delays the freeing of the inode. Introduced with "unfuck proc_sysctl ->d_compare()"
+ * @rcu: delays freeing the header until after an RCU grace period
+ * @unregistering: completion used while unregistering the header
+ * @ctl_table_arg: original legacy ctl_table passed at registration, or NULL
+ * @ctx: copied registration context used to resolve ctl_field entries
+ * @root: sysctl tree containing this header
+ * @set: sysctl set containing this header
+ * @parent: parent directory of this header
+ * @node: array of nodes corresponding to the descriptor entries
+ * @inodes: inodes currently referring to this header
  *
  * @type: Enumeration to differentiate between ctl target types:
  * type.SYSCTL_TABLE_TYPE_DEFAULT: ctl target with no special considerations
  * type.SYSCTL_TABLE_TYPE_PERMANENTLY_EMPTY: Identifies a permanently empty dir
  *                                            target to serve as a mount point
+ * @table_kind: descriptor format stored in this header
+ * @table_kind.SYSCTL_TABLE_KIND_TABLE: legacy ctl_table descriptors
+ * @table_kind.SYSCTL_TABLE_KIND_FIELD: typed ctl_field descriptors
  */
 struct ctl_table_header {
 	union {
 		struct {
-			const struct ctl_table *ctl_table;
+			union {
+				const struct ctl_table *ctl_table;
+				const struct sysctl_field *ctl_fields;
+			};
 			int ctl_table_size;
 			int used;
 			int count;
@@ -275,10 +387,15 @@ struct ctl_table_header {
 	struct ctl_dir *parent;
 	struct ctl_node *node;
 	struct hlist_head inodes; /* head for proc_inode->sysctl_inodes */
+	const struct sysctl_context *ctx;
 	enum {
 		SYSCTL_TABLE_TYPE_DEFAULT,
 		SYSCTL_TABLE_TYPE_PERMANENTLY_EMPTY,
 	} type;
+	enum {
+		SYSCTL_TABLE_KIND_TABLE,
+		SYSCTL_TABLE_KIND_FIELD,
+	} table_kind;
 };
 
 struct ctl_dir {
@@ -303,6 +420,10 @@ struct ctl_table_root {
 #define register_sysctl(path, table)	\
 	register_sysctl_sz(path, table, ARRAY_SIZE(table))
 
+#define register_sysctl_fields(set, path, fields, ctx)			\
+	__register_sysctl_fields(set, path, fields, ARRAY_SIZE(fields),	\
+				 (ctx), sizeof(*(ctx)))
+
 #ifdef CONFIG_SYSCTL
 
 void proc_sys_poll_notify(struct ctl_table_poll *poll);
@@ -315,6 +436,10 @@ extern void retire_sysctl_set(struct ctl_table_set *set);
 struct ctl_table_header *__register_sysctl_table(
 	struct ctl_table_set *set,
 	const char *path, const struct ctl_table *table, size_t table_size);
+struct ctl_table_header *
+__register_sysctl_fields(struct ctl_table_set *set, const char *path,
+			 const struct sysctl_field *fields, size_t field_count,
+			 const struct sysctl_context *ctx, size_t ctx_size);
 struct ctl_table_header *register_sysctl_sz(const char *path, const struct ctl_table *table,
 					    size_t table_size);
 void unregister_sysctl_table(struct ctl_table_header * table);
@@ -350,6 +475,14 @@ static inline struct ctl_table_header *register_sysctl_sz(const char *path,
 	return NULL;
 }
 
+static inline struct ctl_table_header *
+__register_sysctl_fields(struct ctl_table_set *set, const char *path,
+			 const struct sysctl_field *fields, size_t field_count,
+			 const struct sysctl_context *ctx, size_t ctx_size)
+{
+	return NULL;
+}
+
 static inline void unregister_sysctl_table(struct ctl_table_header * table)
 {
 }
Signature · 1 line
2.55.0
Signature · 1 line
Rgrds, legion
#8 lore raw

PATCH v2 4/6 sysctl: ipc: use typed fields for IPC namespace sysctls

In reply to Alexey Gladkov

IPC sysctl registration clones the table and rewrites data and limit pointers for each namespace. This allocates a full ctl_table array per namespace and ties the fixup loop to the table layout.

Describe the table with sysctl_field offsets into ipc_namespace instead. Keep the auto_msgmni entry data-less because its handler selects the effective value itself. This removes the per-namespace table allocation while preserving the existing handlers and limits.

---
 ipc/ipc_sysctl.c | 188 +++++++++++++++++++++--------------------------
 1 file changed, 82 insertions(+), 106 deletions(-)
ipc/ipc_sysctl.c +82 -106
index d038d944257f..3a1a6943e14f 100644
@@ -13,7 +13,6 @@
 #include <linux/capability.h>
 #include <linux/ipc_namespace.h>
 #include <linux/msg.h>
-#include <linux/slab.h>
 #include <linux/cred.h>
 #include "util.h"
 
@@ -41,6 +40,8 @@ static int proc_ipc_auto_msgmni(const struct ctl_table *table, int write,
 
 	memcpy(&ipc_table, table, sizeof(ipc_table));
 	ipc_table.data = &dummy;
+	ipc_table.extra1 = SYSCTL_ZERO;
+	ipc_table.extra2 = SYSCTL_ONE;
 
 	if (write)
 		pr_info_once("writing to auto_msgmni has no effect");
@@ -72,110 +73,129 @@ static int proc_ipc_sem_dointvec(const struct ctl_table *table, int write,
 int ipc_mni = IPCMNI;
 int ipc_mni_shift = IPCMNI_SHIFT;
 int ipc_min_cycle = RADIX_TREE_MAP_SIZE;
+static unsigned int ipc_mni_max = IPCMNI;
+static unsigned int ipc_uint_max = INT_MAX;
 
-static const struct ctl_table ipc_sysctls[] = {
+static const struct sysctl_field ipc_sysctls[] = {
 	{
 		.procname	= "shmmax",
-		.data		= &init_ipc_ns.shm_ctlmax,
-		.maxlen		= sizeof(init_ipc_ns.shm_ctlmax),
 		.mode		= 0644,
-		.proc_handler	= proc_doulongvec_minmax,
+		.type		= SYSCTL_FIELD_SIZE_T,
+		.data_offset	= SYSCTL_FIELD_SIZE_T_OFFSET(struct ipc_namespace,
+							     shm_ctlmax),
 	},
 	{
 		.procname	= "shmall",
-		.data		= &init_ipc_ns.shm_ctlall,
-		.maxlen		= sizeof(init_ipc_ns.shm_ctlall),
 		.mode		= 0644,
-		.proc_handler	= proc_doulongvec_minmax,
+		.type		= SYSCTL_FIELD_SIZE_T,
+		.data_offset	= SYSCTL_FIELD_SIZE_T_OFFSET(struct ipc_namespace,
+							     shm_ctlall),
 	},
 	{
 		.procname	= "shmmni",
-		.data		= &init_ipc_ns.shm_ctlmni,
-		.maxlen		= sizeof(init_ipc_ns.shm_ctlmni),
 		.mode		= 0644,
-		.proc_handler	= proc_dointvec_minmax,
-		.extra1		= SYSCTL_ZERO,
-		.extra2		= &ipc_mni,
+		.type		= SYSCTL_FIELD_INT_MINMAX,
+		.data_offset	= SYSCTL_FIELD_INT_OFFSET(struct ipc_namespace,
+							  shm_ctlmni),
+		.int_limits = {
+			.min	= SYSCTL_ZERO,
+			.max	= &ipc_mni,
+		},
 	},
 	{
 		.procname	= "shm_rmid_forced",
-		.data		= &init_ipc_ns.shm_rmid_forced,
-		.maxlen		= sizeof(init_ipc_ns.shm_rmid_forced),
 		.mode		= 0644,
 		.proc_handler	= proc_ipc_dointvec_minmax_orphans,
-		.extra1		= SYSCTL_ZERO,
-		.extra2		= SYSCTL_ONE,
+		.type		= SYSCTL_FIELD_INT_MINMAX,
+		.data_offset	= SYSCTL_FIELD_INT_OFFSET(struct ipc_namespace,
+							  shm_rmid_forced),
+		.int_limits = {
+			.min	= SYSCTL_ZERO,
+			.max	= SYSCTL_ONE,
+		},
 	},
 	{
 		.procname	= "msgmax",
-		.data		= &init_ipc_ns.msg_ctlmax,
-		.maxlen		= sizeof(init_ipc_ns.msg_ctlmax),
 		.mode		= 0644,
-		.proc_handler	= proc_dointvec_minmax,
-		.extra1		= SYSCTL_ZERO,
-		.extra2		= SYSCTL_INT_MAX,
+		.type		= SYSCTL_FIELD_UINT_MINMAX,
+		.data_offset	= SYSCTL_FIELD_UINT_OFFSET(struct ipc_namespace,
+							   msg_ctlmax),
+		.uint_limits = {
+			.min	= SYSCTL_UINT_ZERO,
+			.max	= &ipc_uint_max,
+		},
 	},
 	{
 		.procname	= "msgmni",
-		.data		= &init_ipc_ns.msg_ctlmni,
-		.maxlen		= sizeof(init_ipc_ns.msg_ctlmni),
 		.mode		= 0644,
-		.proc_handler	= proc_dointvec_minmax,
-		.extra1		= SYSCTL_ZERO,
-		.extra2		= &ipc_mni,
+		.type		= SYSCTL_FIELD_UINT_MINMAX,
+		.data_offset	= SYSCTL_FIELD_UINT_OFFSET(struct ipc_namespace,
+							   msg_ctlmni),
+		.uint_limits = {
+			.min	= SYSCTL_UINT_ZERO,
+			.max	= &ipc_mni_max,
+		},
 	},
 	{
 		.procname	= "auto_msgmni",
-		.data		= NULL,
-		.maxlen		= sizeof(int),
 		.mode		= 0644,
 		.proc_handler	= proc_ipc_auto_msgmni,
-		.extra1		= SYSCTL_ZERO,
-		.extra2		= SYSCTL_ONE,
+		.maxlen		= sizeof(int),
+		.type		= SYSCTL_FIELD_NO_DATA,
 	},
 	{
-		.procname	=  "msgmnb",
-		.data		= &init_ipc_ns.msg_ctlmnb,
-		.maxlen		= sizeof(init_ipc_ns.msg_ctlmnb),
+		.procname	= "msgmnb",
 		.mode		= 0644,
-		.proc_handler	= proc_dointvec_minmax,
-		.extra1		= SYSCTL_ZERO,
-		.extra2		= SYSCTL_INT_MAX,
+		.type		= SYSCTL_FIELD_UINT_MINMAX,
+		.data_offset	= SYSCTL_FIELD_UINT_OFFSET(struct ipc_namespace,
+							   msg_ctlmnb),
+		.uint_limits = {
+			.min	= SYSCTL_UINT_ZERO,
+			.max	= &ipc_uint_max,
+		},
 	},
 	{
 		.procname	= "sem",
-		.data		= &init_ipc_ns.sem_ctls,
-		.maxlen		= 4*sizeof(int),
 		.mode		= 0644,
 		.proc_handler	= proc_ipc_sem_dointvec,
+		.maxlen		= 4 * sizeof(int),
+		.type		= SYSCTL_FIELD_INT,
+		.data_offset	= SYSCTL_FIELD_INT_OFFSET(struct ipc_namespace,
+							  sem_ctls[0]),
 	},
 #ifdef CONFIG_CHECKPOINT_RESTORE
 	{
 		.procname	= "sem_next_id",
-		.data		= &init_ipc_ns.ids[IPC_SEM_IDS].next_id,
-		.maxlen		= sizeof(init_ipc_ns.ids[IPC_SEM_IDS].next_id),
 		.mode		= 0444,
-		.proc_handler	= proc_dointvec_minmax,
-		.extra1		= SYSCTL_ZERO,
-		.extra2		= SYSCTL_INT_MAX,
+		.type		= SYSCTL_FIELD_INT_MINMAX,
+		.data_offset	= SYSCTL_FIELD_INT_OFFSET(struct ipc_namespace,
+							  ids[IPC_SEM_IDS].next_id),
+		.int_limits = {
+			.min	= SYSCTL_ZERO,
+			.max	= SYSCTL_INT_MAX,
+		},
 	},
 	{
 		.procname	= "msg_next_id",
-		.data		= &init_ipc_ns.ids[IPC_MSG_IDS].next_id,
-		.maxlen		= sizeof(init_ipc_ns.ids[IPC_MSG_IDS].next_id),
 		.mode		= 0444,
-		.proc_handler	= proc_dointvec_minmax,
-		.extra1		= SYSCTL_ZERO,
-		.extra2		= SYSCTL_INT_MAX,
+		.type		= SYSCTL_FIELD_INT_MINMAX,
+		.data_offset	= SYSCTL_FIELD_INT_OFFSET(struct ipc_namespace,
+							  ids[IPC_MSG_IDS].next_id),
+		.int_limits = {
+			.min	= SYSCTL_ZERO,
+			.max	= SYSCTL_INT_MAX,
+		},
 	},
 	{
 		.procname	= "shm_next_id",
-		.data		= &init_ipc_ns.ids[IPC_SHM_IDS].next_id,
-		.maxlen		= sizeof(init_ipc_ns.ids[IPC_SHM_IDS].next_id),
 		.mode		= 0444,
-		.proc_handler	= proc_dointvec_minmax,
-		.extra1		= SYSCTL_ZERO,
-		.extra2		= SYSCTL_INT_MAX,
+		.type		= SYSCTL_FIELD_INT_MINMAX,
+		.data_offset	= SYSCTL_FIELD_INT_OFFSET(struct ipc_namespace,
+							  ids[IPC_SHM_IDS].next_id),
+		.int_limits = {
+			.min	= SYSCTL_ZERO,
+			.max	= SYSCTL_INT_MAX,
+		},
 	},
 #endif
 };
@@ -244,57 +264,16 @@ static struct ctl_table_root set_root = {
 
 bool setup_ipc_sysctls(struct ipc_namespace *ns)
 {
-	struct ctl_table *tbl;
+	struct sysctl_context ctx = {
+		.type = SYSCTL_CONTEXT_IPC_NS,
+		.object_size = sizeof(*ns),
+		.ns.ipc_ns = ns,
+	};
 
 	setup_sysctl_set(&ns->ipc_set, &set_root, set_is_seen);
-
-	tbl = kmemdup(ipc_sysctls, sizeof(ipc_sysctls), GFP_KERNEL);
-	if (tbl) {
-		int i;
-
-		for (i = 0; i < ARRAY_SIZE(ipc_sysctls); i++) {
-			if (tbl[i].data == &init_ipc_ns.shm_ctlmax)
-				tbl[i].data = &ns->shm_ctlmax;
-
-			else if (tbl[i].data == &init_ipc_ns.shm_ctlall)
-				tbl[i].data = &ns->shm_ctlall;
-
-			else if (tbl[i].data == &init_ipc_ns.shm_ctlmni)
-				tbl[i].data = &ns->shm_ctlmni;
-
-			else if (tbl[i].data == &init_ipc_ns.shm_rmid_forced)
-				tbl[i].data = &ns->shm_rmid_forced;
-
-			else if (tbl[i].data == &init_ipc_ns.msg_ctlmax)
-				tbl[i].data = &ns->msg_ctlmax;
-
-			else if (tbl[i].data == &init_ipc_ns.msg_ctlmni)
-				tbl[i].data = &ns->msg_ctlmni;
-
-			else if (tbl[i].data == &init_ipc_ns.msg_ctlmnb)
-				tbl[i].data = &ns->msg_ctlmnb;
-
-			else if (tbl[i].data == &init_ipc_ns.sem_ctls)
-				tbl[i].data = &ns->sem_ctls;
-#ifdef CONFIG_CHECKPOINT_RESTORE
-			else if (tbl[i].data == &init_ipc_ns.ids[IPC_SEM_IDS].next_id)
-				tbl[i].data = &ns->ids[IPC_SEM_IDS].next_id;
-
-			else if (tbl[i].data == &init_ipc_ns.ids[IPC_MSG_IDS].next_id)
-				tbl[i].data = &ns->ids[IPC_MSG_IDS].next_id;
-
-			else if (tbl[i].data == &init_ipc_ns.ids[IPC_SHM_IDS].next_id)
-				tbl[i].data = &ns->ids[IPC_SHM_IDS].next_id;
-#endif
-			else
-				tbl[i].data = NULL;
-		}
-
-		ns->ipc_sysctls = __register_sysctl_table(&ns->ipc_set, "kernel", tbl,
-							  ARRAY_SIZE(ipc_sysctls));
-	}
+	ns->ipc_sysctls = register_sysctl_fields(&ns->ipc_set, "kernel",
+						 ipc_sysctls, &ctx);
 	if (!ns->ipc_sysctls) {
-		kfree(tbl);
 		retire_sysctl_set(&ns->ipc_set);
 		return false;
 	}
@@ -304,12 +283,8 @@ bool setup_ipc_sysctls(struct ipc_namespace *ns)
 
 void retire_ipc_sysctls(struct ipc_namespace *ns)
 {
-	const struct ctl_table *tbl;
-
-	tbl = ns->ipc_sysctls->ctl_table_arg;
 	unregister_sysctl_table(ns->ipc_sysctls);
 	retire_sysctl_set(&ns->ipc_set);
-	kfree(tbl);
 }
 
 static int __init ipc_sysctl_init(void)
@@ -326,6 +301,7 @@ device_initcall(ipc_sysctl_init);
 static int __init ipc_mni_extend(char *str)
 {
 	ipc_mni = IPCMNI_EXTEND;
+	ipc_mni_max = IPCMNI_EXTEND;
 	ipc_mni_shift = IPCMNI_EXTEND_SHIFT;
 	ipc_min_cycle = IPCMNI_EXTEND_MIN_CYCLE;
 	pr_info("IPCMNI extended to %d.\n", ipc_mni);
Signature · 1 line
2.55.0
#9 lore raw

PATCH v2 5/6 sysctl: mq: use typed fields for IPC namespace sysctls

In reply to Alexey Gladkov

Convert mq_sysctls. The table can now share one static array across ipc namespaces instead of allocating and rewriting a ctl_table copy for each registration.

---
 ipc/mq_sysctl.c | 104 ++++++++++++++++++++----------------------------
 1 file changed, 43 insertions(+), 61 deletions(-)
ipc/mq_sysctl.c +43 -61
index 0dd12e1c9f53..0d57708e9ea7 100644
@@ -14,55 +14,63 @@
 #include <linux/slab.h>
 #include <linux/cred.h>
 
-static int msg_max_limit_min = MIN_MSGMAX;
-static int msg_max_limit_max = HARD_MSGMAX;
+static unsigned int msg_max_limit_min = MIN_MSGMAX;
+static unsigned int msg_max_limit_max = HARD_MSGMAX;
 
-static int msg_maxsize_limit_min = MIN_MSGSIZEMAX;
-static int msg_maxsize_limit_max = HARD_MSGSIZEMAX;
+static unsigned int msg_maxsize_limit_min = MIN_MSGSIZEMAX;
+static unsigned int msg_maxsize_limit_max = HARD_MSGSIZEMAX;
 
-static const struct ctl_table mq_sysctls[] = {
+static const struct sysctl_field mq_sysctls[] = {
 	{
 		.procname	= "queues_max",
-		.data		= &init_ipc_ns.mq_queues_max,
-		.maxlen		= sizeof(int),
 		.mode		= 0644,
-		.proc_handler	= proc_dointvec,
+		.type		= SYSCTL_FIELD_UINT,
+		.data_offset	= SYSCTL_FIELD_UINT_OFFSET(struct ipc_namespace,
+							   mq_queues_max),
 	},
 	{
 		.procname	= "msg_max",
-		.data		= &init_ipc_ns.mq_msg_max,
-		.maxlen		= sizeof(int),
 		.mode		= 0644,
-		.proc_handler	= proc_dointvec_minmax,
-		.extra1		= &msg_max_limit_min,
-		.extra2		= &msg_max_limit_max,
+		.type		= SYSCTL_FIELD_UINT_MINMAX,
+		.data_offset	= SYSCTL_FIELD_UINT_OFFSET(struct ipc_namespace,
+							   mq_msg_max),
+		.uint_limits = {
+			.min	= &msg_max_limit_min,
+			.max	= &msg_max_limit_max,
+		},
 	},
 	{
 		.procname	= "msgsize_max",
-		.data		= &init_ipc_ns.mq_msgsize_max,
-		.maxlen		= sizeof(int),
 		.mode		= 0644,
-		.proc_handler	= proc_dointvec_minmax,
-		.extra1		= &msg_maxsize_limit_min,
-		.extra2		= &msg_maxsize_limit_max,
+		.type		= SYSCTL_FIELD_UINT_MINMAX,
+		.data_offset	= SYSCTL_FIELD_UINT_OFFSET(struct ipc_namespace,
+							   mq_msgsize_max),
+		.uint_limits = {
+			.min	= &msg_maxsize_limit_min,
+			.max	= &msg_maxsize_limit_max,
+		},
 	},
 	{
 		.procname	= "msg_default",
-		.data		= &init_ipc_ns.mq_msg_default,
-		.maxlen		= sizeof(int),
 		.mode		= 0644,
-		.proc_handler	= proc_dointvec_minmax,
-		.extra1		= &msg_max_limit_min,
-		.extra2		= &msg_max_limit_max,
+		.type		= SYSCTL_FIELD_UINT_MINMAX,
+		.data_offset	= SYSCTL_FIELD_UINT_OFFSET(struct ipc_namespace,
+							   mq_msg_default),
+		.uint_limits = {
+			.min	= &msg_max_limit_min,
+			.max	= &msg_max_limit_max,
+		},
 	},
 	{
 		.procname	= "msgsize_default",
-		.data		= &init_ipc_ns.mq_msgsize_default,
-		.maxlen		= sizeof(int),
 		.mode		= 0644,
-		.proc_handler	= proc_dointvec_minmax,
-		.extra1		= &msg_maxsize_limit_min,
-		.extra2		= &msg_maxsize_limit_max,
+		.type		= SYSCTL_FIELD_UINT_MINMAX,
+		.data_offset	= SYSCTL_FIELD_UINT_OFFSET(struct ipc_namespace,
+							   mq_msgsize_default),
+		.uint_limits = {
+			.min	= &msg_maxsize_limit_min,
+			.max	= &msg_maxsize_limit_max,
+		},
 	},
 };
 
@@ -116,39 +124,17 @@ static struct ctl_table_root set_root = {
 
 bool setup_mq_sysctls(struct ipc_namespace *ns)
 {
-	struct ctl_table *tbl;
+	struct sysctl_context ctx = {
+		.type = SYSCTL_CONTEXT_IPC_NS,
+		.object_size = sizeof(*ns),
+		.ns.ipc_ns = ns,
+	};
 
 	setup_sysctl_set(&ns->mq_set, &set_root, set_is_seen);
 
-	tbl = kmemdup(mq_sysctls, sizeof(mq_sysctls), GFP_KERNEL);
-	if (tbl) {
-		int i;
-
-		for (i = 0; i < ARRAY_SIZE(mq_sysctls); i++) {
-			if (tbl[i].data == &init_ipc_ns.mq_queues_max)
-				tbl[i].data = &ns->mq_queues_max;
-
-			else if (tbl[i].data == &init_ipc_ns.mq_msg_max)
-				tbl[i].data = &ns->mq_msg_max;
-
-			else if (tbl[i].data == &init_ipc_ns.mq_msgsize_max)
-				tbl[i].data = &ns->mq_msgsize_max;
-
-			else if (tbl[i].data == &init_ipc_ns.mq_msg_default)
-				tbl[i].data = &ns->mq_msg_default;
-
-			else if (tbl[i].data == &init_ipc_ns.mq_msgsize_default)
-				tbl[i].data = &ns->mq_msgsize_default;
-			else
-				tbl[i].data = NULL;
-		}
-
-		ns->mq_sysctls = __register_sysctl_table(&ns->mq_set,
-							 "fs/mqueue", tbl,
-							 ARRAY_SIZE(mq_sysctls));
-	}
+	ns->mq_sysctls = register_sysctl_fields(&ns->mq_set, "fs/mqueue",
+						mq_sysctls, &ctx);
 	if (!ns->mq_sysctls) {
-		kfree(tbl);
 		retire_sysctl_set(&ns->mq_set);
 		return false;
 	}
@@ -158,10 +144,6 @@ bool setup_mq_sysctls(struct ipc_namespace *ns)
 
 void retire_mq_sysctls(struct ipc_namespace *ns)
 {
-	const struct ctl_table *tbl;
-
-	tbl = ns->mq_sysctls->ctl_table_arg;
 	unregister_sysctl_table(ns->mq_sysctls);
 	retire_sysctl_set(&ns->mq_set);
-	kfree(tbl);
 }
Signature · 1 line
2.55.0
#10 lore raw

PATCH v2 6/6 sysctl: use typed fields for ucount limits

In reply to Alexey Gladkov

User namespace sysctl registration clones the entire ctl_table and assigns ucount_max entries by table index. This allocates a table for every user namespace and makes the data mapping depend on the descriptor order.

Use typed field offsets to associate each entry explicitly with its ucount_max element. The static descriptor array can then be shared by all user namespaces without allocating or rewriting a ctl_table copy.

---
 kernel/ucount.c | 64 +++++++++++++++++++++++--------------------------
 1 file changed, 30 insertions(+), 34 deletions(-)
kernel/ucount.c +30 -34
index ec8b1445e287..9f71ddefd18e 100644
@@ -64,34 +64,38 @@ static struct ctl_table_root set_root = {
 static long ue_zero = 0;
 static long ue_int_max = INT_MAX;
 
-#define UCOUNT_ENTRY(name)					\
+#define UCOUNT_ENTRY(name, ucount)				\
 	{							\
 		.procname	= name,				\
-		.maxlen		= sizeof(long),			\
 		.mode		= 0644,				\
-		.proc_handler	= proc_doulongvec_minmax,	\
-		.extra1		= &ue_zero,			\
-		.extra2		= &ue_int_max,			\
+		.type		= SYSCTL_FIELD_LONG_MINMAX,	\
+		.data_offset	= SYSCTL_FIELD_LONG_OFFSET(struct user_namespace, \
+							   ucount_max[ucount]),   \
+		.long_limits	= {				\
+			.min	= &ue_zero,			\
+			.max	= &ue_int_max,			\
+		}						\
 	}
-static const struct ctl_table user_table[] = {
-	UCOUNT_ENTRY("max_user_namespaces"),
-	UCOUNT_ENTRY("max_pid_namespaces"),
-	UCOUNT_ENTRY("max_uts_namespaces"),
-	UCOUNT_ENTRY("max_ipc_namespaces"),
-	UCOUNT_ENTRY("max_net_namespaces"),
-	UCOUNT_ENTRY("max_mnt_namespaces"),
-	UCOUNT_ENTRY("max_cgroup_namespaces"),
-	UCOUNT_ENTRY("max_time_namespaces"),
+static const struct sysctl_field user_table[] = {
+	UCOUNT_ENTRY("max_user_namespaces",	UCOUNT_USER_NAMESPACES),
+	UCOUNT_ENTRY("max_pid_namespaces",	UCOUNT_PID_NAMESPACES),
+	UCOUNT_ENTRY("max_uts_namespaces",	UCOUNT_UTS_NAMESPACES),
+	UCOUNT_ENTRY("max_ipc_namespaces",	UCOUNT_IPC_NAMESPACES),
+	UCOUNT_ENTRY("max_net_namespaces",	UCOUNT_NET_NAMESPACES),
+	UCOUNT_ENTRY("max_mnt_namespaces",	UCOUNT_MNT_NAMESPACES),
+	UCOUNT_ENTRY("max_cgroup_namespaces",	UCOUNT_CGROUP_NAMESPACES),
+	UCOUNT_ENTRY("max_time_namespaces",	UCOUNT_TIME_NAMESPACES),
 #ifdef CONFIG_INOTIFY_USER
-	UCOUNT_ENTRY("max_inotify_instances"),
-	UCOUNT_ENTRY("max_inotify_watches"),
+	UCOUNT_ENTRY("max_inotify_instances",	UCOUNT_INOTIFY_INSTANCES),
+	UCOUNT_ENTRY("max_inotify_watches",	UCOUNT_INOTIFY_WATCHES),
 #endif
 #ifdef CONFIG_FANOTIFY
-	UCOUNT_ENTRY("max_fanotify_groups"),
-	UCOUNT_ENTRY("max_fanotify_marks"),
+	UCOUNT_ENTRY("max_fanotify_groups",	UCOUNT_FANOTIFY_GROUPS),
+	UCOUNT_ENTRY("max_fanotify_marks",	UCOUNT_FANOTIFY_MARKS),
 #endif
 #if IS_ENABLED(CONFIG_BINFMT_MISC)
-	UCOUNT_ENTRY("max_binfmt_misc_interpreters"),
+	UCOUNT_ENTRY("max_binfmt_misc_interpreters",
+		     UCOUNT_BINFMT_MISC_INTERPRETERS),
 #endif
 };
 #endif /* CONFIG_SYSCTL */
@@ -99,21 +103,17 @@ static const struct ctl_table user_table[] = {
 bool setup_userns_sysctls(struct user_namespace *ns)
 {
 #ifdef CONFIG_SYSCTL
-	struct ctl_table *tbl;
+	struct sysctl_context ctx = {
+		.type = SYSCTL_CONTEXT_USER_NS,
+		.object_size = sizeof(*ns),
+		.ns.user_ns = ns,
+	};
 
 	BUILD_BUG_ON(ARRAY_SIZE(user_table) != UCOUNT_COUNTS);
 	setup_sysctl_set(&ns->set, &set_root, set_is_seen);
-	tbl = kmemdup(user_table, sizeof(user_table), GFP_KERNEL);
-	if (tbl) {
-		int i;
-		for (i = 0; i < UCOUNT_COUNTS; i++) {
-			tbl[i].data = &ns->ucount_max[i];
-		}
-		ns->sysctls = __register_sysctl_table(&ns->set, "user", tbl,
-						      ARRAY_SIZE(user_table));
-	}
+	ns->sysctls = register_sysctl_fields(&ns->set, "user",
+					     user_table, &ctx);
 	if (!ns->sysctls) {
-		kfree(tbl);
 		retire_sysctl_set(&ns->set);
 		return false;
 	}
@@ -124,12 +124,8 @@ bool setup_userns_sysctls(struct user_namespace *ns)
 void retire_userns_sysctls(struct user_namespace *ns)
 {
 #ifdef CONFIG_SYSCTL
-	const struct ctl_table *tbl;
-
-	tbl = ns->sysctls->ctl_table_arg;
 	unregister_sysctl_table(ns->sysctls);
 	retire_sysctl_set(&ns->set);
-	kfree(tbl);
 #endif
 }
 
Signature · 1 line
2.55.0
maintainer
#11 lore raw
In reply to Alexey Gladkov
Quoted 7 lines · Alexey Gladkov

Hi,

Some sysctl users allocate a private copy of an otherwise static ctl_table for every namespace or device, then rewrite data and limit pointers before registration. Besides the per-instance allocation, these fixups are often addressed by table index, so changing the table can silently associate an entry with the wrong data or limits.

In other words the motivation is to:

1. Remove the per-instance allocation thereby reducing the memory footprint
2. Remove the possibility of mistakenly associating one variable with a different sysctl name (because of un-synced changes)

Only those two. Right?

Quoted 8 lines

This series adds struct sysctl_field as an alternative descriptor for such tables. A field records the value type and an offset into an object selected at registration time. The type-specific offset helpers are small wrappers around offsetof() and only add a compile-time check of the referenced member type. The sysctl core derives the handler, size, data and limits, and builds a temporary ctl_table when invoking existing handler, permission and BPF interfaces.

Building a temporary ctl_table entry just before calling the proc_handler is the way to go, IMO. But do you need to adjust all those members (handler, size, data and limits) always? Sometimes everything stays the same except for the one ".data" member.

Best

PS: Any meat in sashiko's comments?

    https://sashiko.dev/#/patchset/cover.1789987960.git.legion%40kernel.org
Quoted 46 lines

A struct sysctl_context identifies the object shared by the whole registration and is copied into the table header. Subsystems which need more than a namespace can embed it as the first member of a larger context and select the object to which field offsets apply.

Existing ctl_table users are unchanged, and subsystems can migrate one table at a time. Converted tables remain static and read-only instead of being copied and patched for every instance.

Compared with the previous RFC [1], this drops the per-field accessor functions and the macros which generated them. The first two patches prepare the sysctl core without changing its external behaviour. The third patch adds the new descriptor, followed by conversions of the IPC, mqueue and ucount tables as small users of the interface.

This is only the first part of the conversion. Network sysctls and the other subsystem-specific users will be submitted as separate follow-up series so they can be reviewed by their respective maintainers without making this initial series excessively large.

[1] https://lore.kernel.org/all/[email protected]/

Alexey Gladkov (6):
proc: sysctl: address table entries by index
sysctl: add unsigned int limit constants
sysctl: add typed field descriptors
sysctl: ipc: use typed fields for IPC namespace sysctls
sysctl: mq: use typed fields for IPC namespace sysctls
sysctl: use typed fields for ucount limits

 fs/proc/inode.c        |   2 +-
 fs/proc/internal.h     |   2 +-
 fs/proc/proc_sysctl.c  | 601 ++++++++++++++++++++++++++++++-----------
 include/linux/sysctl.h | 150 +++++++++-
 ipc/ipc_sysctl.c       | 188 ++++++-------
 ipc/mq_sysctl.c        | 104 +++----
 kernel/sysctl.c        |   3 +
 kernel/ucount.c        |  64 ++---
 8 files changed, 750 insertions(+), 364 deletions(-)

base-commit: 587858367581b9c55c3690f4e63382ad622719d4

Signature · 1 line
2.55.0
#12 lore raw
Quoted 9 lines · Joel Granados
Quoted 7 lines · Alexey Gladkov

Hi,

Some sysctl users allocate a private copy of an otherwise static ctl_table for every namespace or device, then rewrite data and limit pointers before registration. Besides the per-instance allocation, these fixups are often addressed by table index, so changing the table can silently associate an entry with the wrong data or limits.

Quoted 8 lines

In other words the motivation is to:

1. Remove the per-instance allocation thereby reducing the memory footprint
2. Remove the possibility of mistakenly associating one variable with a different sysctl name (because of un-synced changes)

Only those two. Right?

Another reason is type checking during compile time. Currently, .data, .extra1, and .extra2 are pointers to void. The sysctl_check_table() attempts to validate extra fields, but this is done at runtime and essentially involves checking .maxlen.

I thought that we could not only eliminate the need for memory allocation but also make the parameter checks more strict.

Quoted 9 lines
Quoted 8 lines

This series adds struct sysctl_field as an alternative descriptor for such tables. A field records the value type and an offset into an object selected at registration time. The type-specific offset helpers are small wrappers around offsetof() and only add a compile-time check of the referenced member type. The sysctl core derives the handler, size, data and limits, and builds a temporary ctl_table when invoking existing handler, permission and BPF interfaces.

Quoted 4 lines

Building a temporary ctl_table entry just before calling the proc_handler is the way to go, IMO. But do you need to adjust all those members (handler, size, data and limits) always? Sometimes everything stays the same except for the one ".data" member.

The struct ctl_table/sysctl_field array may contain entries with different types and other fields. I figured it would be easier to reassign the values to keep things simple.

Quoted 2 lines

PS: Any meat in sashiko's comments?

    https://sashiko.dev/#/patchset/cover.1789987960.git.legion%40kernel.org

Hm. For some reason, sashiko has stopped sending review emails. I used to receive them, but now I haven't received anything. I'll check his review and address his comments.

By the way, I heard from Oleg Nesterov that he isn't getting any messages from sashiko either.

Quoted 48 lines
Quoted 46 lines

A struct sysctl_context identifies the object shared by the whole registration and is copied into the table header. Subsystems which need more than a namespace can embed it as the first member of a larger context and select the object to which field offsets apply.

Existing ctl_table users are unchanged, and subsystems can migrate one table at a time. Converted tables remain static and read-only instead of being copied and patched for every instance.

Compared with the previous RFC [1], this drops the per-field accessor functions and the macros which generated them. The first two patches prepare the sysctl core without changing its external behaviour. The third patch adds the new descriptor, followed by conversions of the IPC, mqueue and ucount tables as small users of the interface.

This is only the first part of the conversion. Network sysctls and the other subsystem-specific users will be submitted as separate follow-up series so they can be reviewed by their respective maintainers without making this initial series excessively large.

[1] https://lore.kernel.org/all/[email protected]/

Alexey Gladkov (6):
proc: sysctl: address table entries by index
sysctl: add unsigned int limit constants
sysctl: add typed field descriptors
sysctl: ipc: use typed fields for IPC namespace sysctls
sysctl: mq: use typed fields for IPC namespace sysctls
sysctl: use typed fields for ucount limits

 fs/proc/inode.c        |   2 +-
 fs/proc/internal.h     |   2 +-
 fs/proc/proc_sysctl.c  | 601 ++++++++++++++++++++++++++++++-----------
 include/linux/sysctl.h | 150 +++++++++-
 ipc/ipc_sysctl.c       | 188 ++++++-------
 ipc/mq_sysctl.c        | 104 +++----
 kernel/sysctl.c        |   3 +
 kernel/ucount.c        |  64 ++---
 8 files changed, 750 insertions(+), 364 deletions(-)

base-commit: 587858367581b9c55c3690f4e63382ad622719d4

Signature · 1 line
2.55.0
Signature · 1 line
Rgrds, legion
maintainer
#13 lore raw
In reply to Alexey Gladkov
Quoted 46 lines · Alexey Gladkov

Hi,

Some sysctl users allocate a private copy of an otherwise static ctl_table for every namespace or device, then rewrite data and limit pointers before registration. Besides the per-instance allocation, these fixups are often addressed by table index, so changing the table can silently associate an entry with the wrong data or limits.

This series adds struct sysctl_field as an alternative descriptor for such tables. A field records the value type and an offset into an object selected at registration time. The type-specific offset helpers are small wrappers around offsetof() and only add a compile-time check of the referenced member type. The sysctl core derives the handler, size, data and limits, and builds a temporary ctl_table when invoking existing handler, permission and BPF interfaces.

A struct sysctl_context identifies the object shared by the whole registration and is copied into the table header. Subsystems which need more than a namespace can embed it as the first member of a larger context and select the object to which field offsets apply.

Existing ctl_table users are unchanged, and subsystems can migrate one table at a time. Converted tables remain static and read-only instead of being copied and patched for every instance.

Compared with the previous RFC [1], this drops the per-field accessor functions and the macros which generated them. The first two patches prepare the sysctl core without changing its external behaviour. The third patch adds the new descriptor, followed by conversions of the IPC, mqueue and ucount tables as small users of the interface.

This is only the first part of the conversion. Network sysctls and the other subsystem-specific users will be submitted as separate follow-up series so they can be reviewed by their respective maintainers without making this initial series excessively large.

[1] https://lore.kernel.org/all/[email protected]/

Alexey Gladkov (6):
proc: sysctl: address table entries by index
sysctl: add unsigned int limit constants
sysctl: add typed field descriptors
sysctl: ipc: use typed fields for IPC namespace sysctls
sysctl: mq: use typed fields for IPC namespace sysctls
sysctl: use typed fields for ucount limits

Quoted 9 lines
 fs/proc/inode.c        |   2 +-
 fs/proc/internal.h     |   2 +-
 fs/proc/proc_sysctl.c  | 601 ++++++++++++++++++++++++++++++-----------
 include/linux/sysctl.h | 150 +++++++++-
 ipc/ipc_sysctl.c       | 188 ++++++-------
 ipc/mq_sysctl.c        | 104 +++----
 kernel/sysctl.c        |   3 +
 kernel/ucount.c        |  64 ++---
 8 files changed, 750 insertions(+), 364 deletions(-)

I was thinking more along the lines of [1]. I understand that you have had 2 or 3 versions of this series and it would be great to get your feedback to see if I missed anything obvious

Best

[1] https://lore.kernel.org/all/20260924-lklm-sysctl-headerctx-template-v1-0-b25e51c66ba7@kernel.org/

#14 lore raw

v3 cover sysctl: add typed field descriptors

In reply to Alexey Gladkov

Hi,

Some sysctl users copy an otherwise static ctl_table for each namespace or device, then replace data and limit pointers before registration. Besides the per-instance allocation, these fixups often depend on table indices:
reordering an entry without updating the fixup can silently bind it to the wrong data or limits.

This series lets such users describe a table once with static, read-only sysctl_field entries. Each field names a data type and an offset into an object selected by a sysctl_context at registration time. The offset helpers check the member type at build time. Limits and custom handlers remain optional properties of a field; the field type does not encode a choice of handler or whether limits are present. A subsystem that needs more than a namespace can embed sysctl_context in its own registration context.

The sysctl core keeps the context with the table header and materializes a ctl_table entry when an existing handler, permission check or BPF hook needs one. Legacy ctl_table registrations retain their own path, and subsystems can convert tables independently. The IPC, mqueue and ucount conversions here remove table copies without changing their backing data.

Changes since v2:

- Drop sysctl_uint_vals, as requested by Joel. Existing limit constants suffice for these conversions.
- Drop the *_MINMAX field types. A field's type now describes its data, while optional min/max pointers supply bounds to the selected handler.
- Keep __register_sysctl_table() and __register_sysctl_fields() as separate registration paths, sharing only allocation and tree insertion helpers.
- Address the sashiko findings: avoid calling mode_fn under sysctl_lock, validate the effective maxlen against the context object, and reject NO_DATA fields paired with standard handlers that require data.

This is the first part of the conversion. Network sysctls and other subsystem-specific users will follow in separate series for review by their maintainers.

Alexey Gladkov (5):
proc: sysctl: address table entries by index
sysctl: add typed field descriptors
sysctl: ipc: use typed fields for IPC namespace sysctls
sysctl: mq: use typed fields for IPC namespace sysctls
sysctl: use typed fields for ucount limits

 fs/proc/inode.c        |   2 +-
 fs/proc/internal.h     |   2 +-
 fs/proc/proc_sysctl.c  | 681 +++++++++++++++++++++++++++++------------
 include/linux/sysctl.h | 138 ++++++++-
 ipc/ipc_sysctl.c       | 189 +++++-------
 ipc/mq_sysctl.c        | 104 +++----
 kernel/ucount.c        |  64 ++--
 7 files changed, 772 insertions(+), 408 deletions(-)

base-commit: 587858367581b9c55c3690f4e63382ad622719d4

Signature · 1 line
2.55.0
#15 lore raw

PATCH v3 1/5 proc: sysctl: address table entries by index

proc_sysctl keeps pointers to ctl_table entries in several lookup and inode paths. That makes it hard to support alternative descriptor formats because the tree logic assumes that every registered entry already is a ctl_table object.

Store the entry index in proc inodes and pass header/index pairs through the lookup, iteration and permission paths instead. The existing ctl_table entries are still used for all handler, permission and poll operations, but the core no longer needs to carry a direct ctl_table pointer through every internal path.

---
 fs/proc/inode.c       |   2 +-
 fs/proc/internal.h    |   2 +-
 fs/proc/proc_sysctl.c | 330 ++++++++++++++++++++++++------------------
 3 files changed, 194 insertions(+), 140 deletions(-)
fs/proc/inode.c +1 -1
index b7634f975d98..c735338ecc8d 100644
@@ -62,7 +62,7 @@ static struct inode *proc_alloc_inode(struct super_block *sb)
 	ei->op.proc_get_link = NULL;
 	ei->pde = NULL;
 	ei->sysctl = NULL;
-	ei->sysctl_entry = NULL;
+	ei->sysctl_entry_idx = 0;
 	INIT_HLIST_NODE(&ei->sibling_inodes);
 	ei->ns_ops = NULL;
 	return &ei->vfs_inode;
fs/proc/internal.h +1 -1
index 04bd6c9e65a7..7fc730b0d7ab 100644
@@ -123,7 +123,7 @@ struct proc_inode {
 	union proc_op op;
 	struct proc_dir_entry *pde;
 	struct ctl_table_header *sysctl;
-	const struct ctl_table *sysctl_entry;
+	size_t sysctl_entry_idx;
 	struct hlist_node sibling_inodes;
 	const struct proc_ns_operations *ns_ops;
 	struct inode vfs_inode;
fs/proc/proc_sysctl.c +192 -138

763 lines not on this page · raw message

Signature · 1 line
2.55.0
#16 lore raw

PATCH v3 2/5 sysctl: add typed field descriptors

In reply to Alexey Gladkov

Several sysctl users duplicate ctl_table arrays at registration time so data and limit pointers can be redirected to namespace or device state. The copies consume memory for every instance and their index-based fixups silently depend on the source table order.

Add sysctl_field as an alternative static descriptor. A field records the value kind and a checked offset into an object selected by a registration context. Type-specific offset helpers verify the backing member type at build time, while the core derives the legacy proc handler, size and limit pointers from the field kind.

Keep ctl_table as the interface used by proc handlers, permissions and BPF by materializing one entry on the stack when those paths need it. Existing ctl_table registrations are unchanged, and converted users can share one read-only descriptor array without allocating a table copy.

---
 fs/proc/proc_sysctl.c  | 389 ++++++++++++++++++++++++++++++++---------
 include/linux/sysctl.h | 138 ++++++++++++++-
 2 files changed, 441 insertions(+), 86 deletions(-)
fs/proc/proc_sysctl.c +308 -81
index b5cb219bcdbc..565d0400121c 100644
@@ -18,6 +18,7 @@
 #include <linux/mount.h>
 #include <linux/kmemleak.h>
 #include <linux/lockdep.h>
+#include <linux/overflow.h>
 #include "internal.h"
 
 #define list_for_each_table_entry(index, header)			\
@@ -91,28 +92,173 @@ static int sysctl_follow_link(struct ctl_table_header **phead, size_t *pindex);
 static int insert_links(struct ctl_table_header *head);
 static void put_links(struct ctl_table_header *header);
 
+static inline bool is_field_table(const struct ctl_table_header *head)
+{
+	return head->table_kind == SYSCTL_TABLE_KIND_FIELD;
+}
+
 static const char *sysctl_entry_procname(struct ctl_table_header *head,
 					 size_t index)
 {
+	if (is_field_table(head))
+		return head->ctl_fields[index].procname;
+
 	return head->ctl_table[index].procname;
 }
 
 static umode_t sysctl_entry_mode(struct ctl_table_header *head, size_t index)
 {
-	return head->ctl_table[index].mode;
+	const struct sysctl_field *field;
+
+	if (!is_field_table(head))
+		return head->ctl_table[index].mode;
+
+	field = &head->ctl_fields[index];
+
+	if (field->mode_fn) {
+		lockdep_assert_not_held(&sysctl_lock);
+		return field->mode_fn(head->ctx);
+	}
+
+	return field->mode;
+}
+
+static bool sysctl_entry_is_dir(const struct ctl_table_header *head,
+				size_t index)
+{
+	/* Do not call field mode_fn callback while holding sysctl_lock. */
+	if (is_field_table(head))
+		return false;
+
+	return S_ISDIR(head->ctl_table[index].mode);
+}
+
+static bool sysctl_entry_is_link(const struct ctl_table_header *head,
+				 size_t index)
+{
+	if (is_field_table(head))
+		return false;
+
+	return S_ISLNK(head->ctl_table[index].mode);
 }
 
 static struct ctl_table_poll *sysctl_entry_poll(struct ctl_table_header *head,
 						size_t index)
 {
+	if (is_field_table(head))
+		return NULL;
+
 	return head->ctl_table[index].poll;
 }
 
+static void *sysctl_context_object(const struct sysctl_context *ctx)
+{
+	if (!ctx)
+		return NULL;
+	if (ctx->object)
+		return ctx->object(ctx);
+
+	switch (ctx->type) {
+	case SYSCTL_CONTEXT_USER_NS:
+		return ctx->ns.user_ns;
+	case SYSCTL_CONTEXT_IPC_NS:
+		return ctx->ns.ipc_ns;
+	case SYSCTL_CONTEXT_PID_NS:
+		return ctx->ns.pid_ns;
+	case SYSCTL_CONTEXT_NET_NS:
+		return ctx->ns.net_ns;
+	}
+
+	return NULL;
+}
+
+static void *sysctl_context_data(const struct sysctl_context *ctx,
+				 size_t offset, size_t size)
+{
+	void *object = sysctl_context_object(ctx);
+
+	if (!object || offset > ctx->object_size ||
+	    size > ctx->object_size - offset)
+		return NULL;
+
+	return (char *)object + offset;
+}
+
 static const struct ctl_table *
 sysctl_entry_table(struct ctl_table_header *head, size_t index,
 		   struct ctl_table *table)
 {
-	return &head->ctl_table[index];
+	const struct sysctl_field *field;
+
+	if (!is_field_table(head))
+		return &head->ctl_table[index];
+
+	field = &head->ctl_fields[index];
+
+	memset(table, 0, sizeof(*table));
+	table->procname = field->procname;
+	table->mode = field->mode;
+
+	if (field->mode_fn) {
+		lockdep_assert_not_held(&sysctl_lock);
+		table->mode = field->mode_fn(head->ctx);
+	}
+
+	switch (field->type) {
+	case SYSCTL_FIELD_NO_DATA:
+		break;
+	case SYSCTL_FIELD_STRING:
+		table->proc_handler = proc_dostring;
+		table->maxlen = field->maxlen;
+		break;
+	case SYSCTL_FIELD_BOOL:
+		table->proc_handler = proc_dobool;
+		table->maxlen = sizeof(bool);
+		break;
+	case SYSCTL_FIELD_U8:
+		table->proc_handler = proc_dou8vec_minmax;
+		table->maxlen = sizeof(u8);
+		table->extra1 = field->u8_limits.min;
+		table->extra2 = field->u8_limits.max;
+		break;
+	case SYSCTL_FIELD_INT:
+		table->proc_handler = proc_dointvec_minmax;
+		table->maxlen = sizeof(int);
+		table->extra1 = field->int_limits.min;
+		table->extra2 = field->int_limits.max;
+		break;
+	case SYSCTL_FIELD_UINT:
+		table->proc_handler = proc_douintvec_minmax;
+		table->maxlen = sizeof(unsigned int);
+		table->extra1 = field->uint_limits.min;
+		table->extra2 = field->uint_limits.max;
+		break;
+	case SYSCTL_FIELD_LONG:
+		table->proc_handler = proc_doulongvec_minmax;
+		table->maxlen = sizeof(long);
+		table->extra1 = field->long_limits.min;
+		table->extra2 = field->long_limits.max;
+		break;
+	case SYSCTL_FIELD_ULONG:
+		table->proc_handler = proc_doulongvec_minmax;
+		table->maxlen = sizeof(unsigned long);
+		table->extra1 = field->ulong_limits.min;
+		table->extra2 = field->ulong_limits.max;
+		break;
+	case SYSCTL_FIELD_SIZE_T:
+		table->proc_handler = proc_doulongvec_minmax;
+		table->maxlen = sizeof(size_t);
+		break;
+	}
+
+	if (field->proc_handler)
+		table->proc_handler = field->proc_handler;
+	if (field->maxlen)
+		table->maxlen = field->maxlen;
+	if (field->type != SYSCTL_FIELD_NO_DATA)
+		table->data = sysctl_context_data(head->ctx, field->data_offset, table->maxlen);
+
+	return table;
 }
 
 static void sysctl_print_dir(struct ctl_dir *dir)
@@ -212,9 +358,17 @@ static void erase_entry(struct ctl_table_header *head, size_t index)
 
 static void init_header(struct ctl_table_header *head,
 	struct ctl_table_root *root, struct ctl_table_set *set,
-	struct ctl_node *node, const struct ctl_table *table, size_t table_size)
+	struct ctl_node *node, const struct ctl_table *table,
+	const struct sysctl_field *fields, size_t table_size,
+	const struct sysctl_context *ctx)
 {
-	head->ctl_table = table;
+	if (fields) {
+		head->ctl_fields = fields;
+		head->table_kind = SYSCTL_TABLE_KIND_FIELD;
+	} else {
+		head->ctl_table = table;
+		head->table_kind = SYSCTL_TABLE_KIND_TABLE;
+	}
 	head->ctl_table_size = table_size;
 	head->ctl_table_arg = table;
 	head->used = 0;
@@ -223,6 +377,7 @@ static void init_header(struct ctl_table_header *head,
 	head->unregistering = NULL;
 	head->root = root;
 	head->set = set;
+	head->ctx = ctx;
 	head->parent = NULL;
 	head->node = node;
 	INIT_HLIST_HEAD(&head->inodes);
@@ -981,7 +1136,7 @@ static struct ctl_dir *find_subdir(struct ctl_dir *dir,
 
 	if (!find_entry(&head, &index, dir, name, namelen))
 		return ERR_PTR(-ENOENT);
-	if (!S_ISDIR(sysctl_entry_mode(head, index)))
+	if (!sysctl_entry_is_dir(head, index))
 		return ERR_PTR(-ENOTDIR);
 	return container_of(head, struct ctl_dir, header);
 }
@@ -1006,7 +1161,8 @@ static struct ctl_dir *new_dir(struct ctl_table_set *set,
 	memcpy(new_name, name, namelen);
 	table[0].procname = new_name;
 	table[0].mode = S_IFDIR|S_IRUGO|S_IXUGO;
-	init_header(&new->header, set->dir.header.root, set, node, table, 1);
+	init_header(&new->header, set->dir.header.root, set, node, table, NULL,
+		    1, NULL);
 
 	return new;
 }
@@ -1187,6 +1343,10 @@ static int sysctl_check_table(const char *path, struct ctl_table_header *header)
 		entry = sysctl_entry_table(header, index, &table);
 		if (!entry->procname)
 			err |= sysctl_err(path, entry, "procname is null");
+		if (is_field_table(header) &&
+		    header->ctl_fields[index].type != SYSCTL_FIELD_NO_DATA &&
+		    !entry->data)
+			err |= sysctl_err(path, entry, "No data");
 		if ((entry->proc_handler == proc_dostring) ||
 		    (entry->proc_handler == proc_dobool) ||
 		    (entry->proc_handler == proc_dointvec) ||
@@ -1199,7 +1359,9 @@ static int sysctl_check_table(const char *path, struct ctl_table_header *header)
 		    (entry->proc_handler == proc_dointvec_ms_jiffies) ||
 		    (entry->proc_handler == proc_doulongvec_minmax) ||
 		    (entry->proc_handler == proc_doulongvec_ms_jiffies_minmax)) {
-			if (!entry->data)
+			if (!entry->data &&
+			    (!is_field_table(header) ||
+			     header->ctl_fields[index].type == SYSCTL_FIELD_NO_DATA))
 				err |= sysctl_err(path, entry, "No data");
 			if (!entry->maxlen)
 				err |= sysctl_err(path, entry, "No maxlen");
@@ -1255,7 +1417,7 @@ static struct ctl_table_header *new_links(struct ctl_dir *dir, struct ctl_table_
 		link++;
 	}
 	init_header(links, dir->header.root, dir->header.set, node, link_table,
-		    head->ctl_table_size);
+		    NULL, head->ctl_table_size, NULL);
 	links->nreg = head->ctl_table_size;
 
 	return links;
@@ -1279,10 +1441,10 @@ static bool get_links(struct ctl_dir *dir,
 		if (!find_entry(&tmp_head, &link_index, dir, procname,
 				strlen(procname)))
 			return false;
-		if (S_ISDIR(sysctl_entry_mode(tmp_head, link_index)) &&
-		    S_ISDIR(sysctl_entry_mode(header, index)))
+		if (sysctl_entry_is_dir(tmp_head, link_index) &&
+		    sysctl_entry_is_dir(header, index))
 			continue;
-		if (S_ISLNK(sysctl_entry_mode(tmp_head, link_index)) &&
+		if (sysctl_entry_is_link(tmp_head, link_index) &&
 		    tmp_head->ctl_table[link_index].data == link_root)
 			continue;
 		return false;
@@ -1368,68 +1530,45 @@ static struct ctl_dir *sysctl_mkdir_p(struct ctl_dir *dir, const char *path)
 	return dir;
 }
 
-/**
- * __register_sysctl_table - register a leaf sysctl table
- * @set: Sysctl tree to register on
- * @path: The path to the directory the sysctl table is in.
- *
- * @table: the top-level table structure. This table should not be free'd
- *         after registration. So it should not be used on stack. It can either
- *         be a global or dynamically allocated by the caller and free'd later
- *         after sysctl unregistration.
- * @table_size : The number of elements in table
- *
- * Register a sysctl table hierarchy. @table should be a filled in ctl_table
- * array.
- *
- * The members of the &struct ctl_table structure are used as follows:
- * procname - the name of the sysctl file under /proc/sys. Set to %NULL to not
- *            enter a sysctl file
- * data     - a pointer to data for use by proc_handler
- * maxlen   - the maximum size in bytes of the data
- * mode     - the file permissions for the /proc/sys file
- * type     - Defines the target type (described in struct definition)
- * proc_handler - the text handler routine (described below)
- *
- * extra1, extra2 - extra pointers usable by the proc handler routines
- * XXX: we should eventually modify these to use long min / max [0]
- * [0] https://lkml.kernel.org/[email protected]
- *
- * Leaf nodes in the sysctl tree will be represented by a single file
- * under /proc; non-leaf nodes are not allowed.
- *
- * There must be a proc_handler routine for any terminal nodes.
- * Several default handlers are available to cover common cases -
- *
- * proc_dostring(), proc_dointvec(), proc_dointvec_jiffies(),
- * proc_dointvec_userhz_jiffies(), proc_dointvec_minmax(),
- * proc_doulongvec_ms_jiffies_minmax(), proc_doulongvec_minmax()
- *
- * It is the handler's job to read the input buffer from user memory
- * and process it. The handler should return 0 on success.
- *
- * This routine returns %NULL on a failure to register, and a pointer
- * to the table header on success.
- */
-struct ctl_table_header *__register_sysctl_table(
-	struct ctl_table_set *set,
-	const char *path, const struct ctl_table *table, size_t table_size)
+static struct ctl_table_header *alloc_sysctl_header(size_t table_size,
+						    size_t extra_size,
+						    size_t *context_offset)
 {
-	struct ctl_table_root *root = set->dir.header.root;
 	struct ctl_table_header *header;
-	struct ctl_dir *dir;
-	struct ctl_node *node;
-	size_t alloc_size;
+	size_t nodes_size, alloc_size, offset;
 
-	alloc_size = sizeof(struct ctl_table_header) +
-		     sizeof(struct ctl_node) * table_size;
+	if (check_mul_overflow(sizeof(struct ctl_node), table_size, &nodes_size))
+		return NULL;
 
-	header = kzalloc(alloc_size, GFP_KERNEL_ACCOUNT);
-	if (!header)
+	if (check_add_overflow(sizeof(*header), nodes_size, &offset))
 		return NULL;
 
-	node = (struct ctl_node *)(header + 1);
-	init_header(header, root, set, node, table, table_size);
+	/*
+	 * Store the copied context after the ctl_node array. struct sysctl_context
+	 * is the first member of any caller-defined wrapper, whose alignment
+	 * must not exceed that of struct sysctl_context.
+	 */
+	if (context_offset) {
+		if (check_add_overflow(offset,
+				       __alignof__(struct sysctl_context) - 1,
+				       &offset))
+			return NULL;
+		offset = ALIGN_DOWN(offset, __alignof__(struct sysctl_context));
+		*context_offset = offset;
+	}
+
+	if (check_add_overflow(offset, extra_size, &alloc_size))
+		return NULL;
+
+	return kzalloc(alloc_size, GFP_KERNEL_ACCOUNT);
+}
+
+static struct ctl_table_header *register_sysctl_header(struct ctl_table_set *set,
+						       const char *path,
+						       struct ctl_table_header *header)
+{
+	struct ctl_dir *dir;
+
 	if (sysctl_check_table(path, header))
 		goto fail;
 
@@ -1459,6 +1598,98 @@ struct ctl_table_header *__register_sysctl_table(
 	return NULL;
 }
 
+/**
+ * __register_sysctl_fields - register a table of typed sysctl fields
+ * @set: Sysctl tree to register on
+ * @path: Path to the directory containing the fields
+ * @fields: Field descriptors, kept alive until unregistration
+ * @field_count: Number of descriptors in @fields
+ * @ctx: Context selecting the object containing the field data
+ * @ctx_size: Size of @ctx, including any wrapper object that embeds it
+ *
+ * @fields must remain available until unregistration. The registration copies
+ * @ctx into the table header, so a stack-allocated context is allowed. If
+ * @ctx points to a wrapper object, &struct sysctl_context must be its first
+ * member and its alignment must not exceed that of &struct sysctl_context.
+ * Field data offsets are checked against @ctx->object_size and applied to the
+ * namespace selected by @ctx->ns, or to the object returned by @ctx->object.
+ *
+ * Return: The registered header, or %NULL on failure.
+ */
+struct ctl_table_header *__register_sysctl_fields(struct ctl_table_set *set,
+						  const char *path,
+						  const struct sysctl_field *fields, size_t field_count,
+						  const struct sysctl_context *ctx, size_t ctx_size)
+{
+	struct ctl_table_header *header;
+	struct sysctl_context *header_ctx;
+	struct ctl_node *node;
+	size_t context_offset;
+
+	if (!fields || !ctx || ctx_size < sizeof(*ctx) ||
+	    !ctx->object_size || !sysctl_context_object(ctx))
+		return NULL;
+
+	header = alloc_sysctl_header(field_count, ctx_size, &context_offset);
+	if (!header)
+		return NULL;
+
+	header_ctx = (void *)header + context_offset;
+	memcpy(header_ctx, ctx, ctx_size);
+
+	node = (struct ctl_node *)(header + 1);
+
+	init_header(header, set->dir.header.root, set, node, NULL, fields,
+		    field_count, header_ctx);
+
+	return register_sysctl_header(set, path, header);
+}
+EXPORT_SYMBOL(__register_sysctl_fields);
+
+/**
+ * __register_sysctl_table - register a leaf sysctl table
+ * @set: Sysctl tree to register on
+ * @path: Path to the directory containing the table
+ * @table: ctl_table array, kept alive until unregistration
+ * @table_size: Number of entries in @table
+ *
+ * Register a table of leaf entries under @path. The caller owns @table and
+ * must keep it available until unregistration. It may be static or allocated,
+ * but must not be on the stack.
+ *
+ * Each entry's procname names the file under /proc/sys, data points to the
+ * value passed to proc_handler, maxlen bounds that value, and mode gives its
+ * permissions. A leaf entry needs a proc_handler; standard handlers include
+ * proc_dostring(), proc_dointvec(), proc_dointvec_minmax() and
+ * proc_doulongvec_minmax(). The handler reads or writes the user buffer and
+ * returns zero on success. The extra1 and extra2 pointers carry additional
+ * handler arguments, commonly the minimum and maximum values.
+ *
+ * Return: The registered header, or %NULL on failure.
+ */
+struct ctl_table_header *__register_sysctl_table(struct ctl_table_set *set,
+						 const char *path,
+						 const struct ctl_table *table, size_t table_size)
+{
+	struct ctl_table_header *header;
+	struct ctl_node *node;
+
+	if (!table)
+		return NULL;
+
+	header = alloc_sysctl_header(table_size, 0, NULL);
+	if (!header)
+		return NULL;
+
+	node = (struct ctl_node *)(header + 1);
+
+	init_header(header, set->dir.header.root, set, node, table, NULL,
+		    table_size, NULL);
+
+	return register_sysctl_header(set, path, header);
+}
+EXPORT_SYMBOL(__register_sysctl_table);
+
 /**
  * register_sysctl_sz - register a sysctl table
  * @path: The path to the directory the sysctl table is in. If the path
@@ -1535,20 +1766,16 @@ static void put_links(struct ctl_table_header *header)
 
 	list_for_each_table_entry(index, header) {
 		struct ctl_table_header *link_head;
-		struct ctl_table table;
-		const struct ctl_table *link;
 		const char *name = sysctl_entry_procname(header, index);
-
-		if (!find_entry(&link_head, &link_index, core_parent, name,
-				strlen(name)))
-			link = NULL;
-		else
-			link = sysctl_entry_table(link_head, link_index, &table);
-
-		if (link &&
-		    ((S_ISDIR(link->mode) &&
-		      S_ISDIR(sysctl_entry_mode(header, index))) ||
-		     (S_ISLNK(link->mode) && (link->data == root)))) {
+		bool found;
+
+		found = find_entry(&link_head, &link_index, core_parent, name,
+				   strlen(name));
+		if (found &&
+		    ((sysctl_entry_is_dir(link_head, link_index) &&
+		      sysctl_entry_is_dir(header, index)) ||
+		     (sysctl_entry_is_link(link_head, link_index) &&
+		      link_head->ctl_table[link_index].data == root))) {
 			drop_sysctl_table(link_head);
 		} else {
 			pr_err("sysctl link missing during unregister: ");
@@ -1603,7 +1830,7 @@ void setup_sysctl_set(struct ctl_table_set *set,
 {
 	memset(set, 0, sizeof(*set));
 	set->is_seen = is_seen;
-	init_header(&set->dir.header, root, set, NULL, root_table, 1);
+	init_header(&set->dir.header, root, set, NULL, root_table, NULL, 1, NULL);
 }
 
 void retire_sysctl_set(struct ctl_table_set *set)
include/linux/sysctl.h +133 -5

217 lines not on this page · raw message

Signature · 1 line
2.55.0
#17 lore raw

PATCH v3 3/5 sysctl: ipc: use typed fields for IPC namespace sysctls

In reply to Alexey Gladkov

IPC sysctl registration clones the table and rewrites data and limit pointers for each namespace. This allocates a full ctl_table array per namespace and ties the fixup loop to the table layout.

Describe the table with sysctl_field offsets into ipc_namespace instead. Keep the auto_msgmni entry data-less because its handler selects the effective value itself. This removes the per-namespace table allocation while preserving the existing handlers and limits.

---
 ipc/ipc_sysctl.c | 189 +++++++++++++++++++++--------------------------
 1 file changed, 83 insertions(+), 106 deletions(-)
ipc/ipc_sysctl.c +83 -106
index d038d944257f..527862e9e0e4 100644
@@ -13,7 +13,6 @@
 #include <linux/capability.h>
 #include <linux/ipc_namespace.h>
 #include <linux/msg.h>
-#include <linux/slab.h>
 #include <linux/cred.h>
 #include "util.h"
 
@@ -41,6 +40,8 @@ static int proc_ipc_auto_msgmni(const struct ctl_table *table, int write,
 
 	memcpy(&ipc_table, table, sizeof(ipc_table));
 	ipc_table.data = &dummy;
+	ipc_table.extra1 = SYSCTL_ZERO;
+	ipc_table.extra2 = SYSCTL_ONE;
 
 	if (write)
 		pr_info_once("writing to auto_msgmni has no effect");
@@ -72,110 +73,130 @@ static int proc_ipc_sem_dointvec(const struct ctl_table *table, int write,
 int ipc_mni = IPCMNI;
 int ipc_mni_shift = IPCMNI_SHIFT;
 int ipc_min_cycle = RADIX_TREE_MAP_SIZE;
+static unsigned int ipc_mni_max = IPCMNI;
+static unsigned int ipc_uint_zero = 0;
+static unsigned int ipc_uint_max = INT_MAX;
 
-static const struct ctl_table ipc_sysctls[] = {
+static const struct sysctl_field ipc_sysctls[] = {
 	{
 		.procname	= "shmmax",
-		.data		= &init_ipc_ns.shm_ctlmax,
-		.maxlen		= sizeof(init_ipc_ns.shm_ctlmax),
 		.mode		= 0644,
-		.proc_handler	= proc_doulongvec_minmax,
+		.type		= SYSCTL_FIELD_SIZE_T,
+		.data_offset	= SYSCTL_FIELD_SIZE_T_OFFSET(struct ipc_namespace,
+							     shm_ctlmax),
 	},
 	{
 		.procname	= "shmall",
-		.data		= &init_ipc_ns.shm_ctlall,
-		.maxlen		= sizeof(init_ipc_ns.shm_ctlall),
 		.mode		= 0644,
-		.proc_handler	= proc_doulongvec_minmax,
+		.type		= SYSCTL_FIELD_SIZE_T,
+		.data_offset	= SYSCTL_FIELD_SIZE_T_OFFSET(struct ipc_namespace,
+							     shm_ctlall),
 	},
 	{
 		.procname	= "shmmni",
-		.data		= &init_ipc_ns.shm_ctlmni,
-		.maxlen		= sizeof(init_ipc_ns.shm_ctlmni),
 		.mode		= 0644,
-		.proc_handler	= proc_dointvec_minmax,
-		.extra1		= SYSCTL_ZERO,
-		.extra2		= &ipc_mni,
+		.type		= SYSCTL_FIELD_INT,
+		.data_offset	= SYSCTL_FIELD_INT_OFFSET(struct ipc_namespace,
+							  shm_ctlmni),
+		.int_limits = {
+			.min	= SYSCTL_ZERO,
+			.max	= &ipc_mni,
+		},
 	},
 	{
 		.procname	= "shm_rmid_forced",
-		.data		= &init_ipc_ns.shm_rmid_forced,
-		.maxlen		= sizeof(init_ipc_ns.shm_rmid_forced),
 		.mode		= 0644,
 		.proc_handler	= proc_ipc_dointvec_minmax_orphans,
-		.extra1		= SYSCTL_ZERO,
-		.extra2		= SYSCTL_ONE,
+		.type		= SYSCTL_FIELD_INT,
+		.data_offset	= SYSCTL_FIELD_INT_OFFSET(struct ipc_namespace,
+							  shm_rmid_forced),
+		.int_limits = {
+			.min	= SYSCTL_ZERO,
+			.max	= SYSCTL_ONE,
+		},
 	},
 	{
 		.procname	= "msgmax",
-		.data		= &init_ipc_ns.msg_ctlmax,
-		.maxlen		= sizeof(init_ipc_ns.msg_ctlmax),
 		.mode		= 0644,
-		.proc_handler	= proc_dointvec_minmax,
-		.extra1		= SYSCTL_ZERO,
-		.extra2		= SYSCTL_INT_MAX,
+		.type		= SYSCTL_FIELD_UINT,
+		.data_offset	= SYSCTL_FIELD_UINT_OFFSET(struct ipc_namespace,
+							   msg_ctlmax),
+		.uint_limits = {
+			.min	= &ipc_uint_zero,
+			.max	= &ipc_uint_max,
+		},
 	},
 	{
 		.procname	= "msgmni",
-		.data		= &init_ipc_ns.msg_ctlmni,
-		.maxlen		= sizeof(init_ipc_ns.msg_ctlmni),
 		.mode		= 0644,
-		.proc_handler	= proc_dointvec_minmax,
-		.extra1		= SYSCTL_ZERO,
-		.extra2		= &ipc_mni,
+		.type		= SYSCTL_FIELD_UINT,
+		.data_offset	= SYSCTL_FIELD_UINT_OFFSET(struct ipc_namespace,
+							   msg_ctlmni),
+		.uint_limits = {
+			.min	= &ipc_uint_zero,
+			.max	= &ipc_mni_max,
+		},
 	},
 	{
 		.procname	= "auto_msgmni",
-		.data		= NULL,
-		.maxlen		= sizeof(int),
 		.mode		= 0644,
 		.proc_handler	= proc_ipc_auto_msgmni,
-		.extra1		= SYSCTL_ZERO,
-		.extra2		= SYSCTL_ONE,
+		.maxlen		= sizeof(int),
+		.type		= SYSCTL_FIELD_NO_DATA,
 	},
 	{
-		.procname	=  "msgmnb",
-		.data		= &init_ipc_ns.msg_ctlmnb,
-		.maxlen		= sizeof(init_ipc_ns.msg_ctlmnb),
+		.procname	= "msgmnb",
 		.mode		= 0644,
-		.proc_handler	= proc_dointvec_minmax,
-		.extra1		= SYSCTL_ZERO,
-		.extra2		= SYSCTL_INT_MAX,
+		.type		= SYSCTL_FIELD_UINT,
+		.data_offset	= SYSCTL_FIELD_UINT_OFFSET(struct ipc_namespace,
+							   msg_ctlmnb),
+		.uint_limits = {
+			.min	= &ipc_uint_zero,
+			.max	= &ipc_uint_max,
+		},
 	},
 	{
 		.procname	= "sem",
-		.data		= &init_ipc_ns.sem_ctls,
-		.maxlen		= 4*sizeof(int),
 		.mode		= 0644,
 		.proc_handler	= proc_ipc_sem_dointvec,
+		.maxlen		= 4 * sizeof(int),
+		.type		= SYSCTL_FIELD_INT,
+		.data_offset	= SYSCTL_FIELD_INT_OFFSET(struct ipc_namespace,
+							  sem_ctls[0]),
 	},
 #ifdef CONFIG_CHECKPOINT_RESTORE
 	{
 		.procname	= "sem_next_id",
-		.data		= &init_ipc_ns.ids[IPC_SEM_IDS].next_id,
-		.maxlen		= sizeof(init_ipc_ns.ids[IPC_SEM_IDS].next_id),
 		.mode		= 0444,
-		.proc_handler	= proc_dointvec_minmax,
-		.extra1		= SYSCTL_ZERO,
-		.extra2		= SYSCTL_INT_MAX,
+		.type		= SYSCTL_FIELD_INT,
+		.data_offset	= SYSCTL_FIELD_INT_OFFSET(struct ipc_namespace,
+							  ids[IPC_SEM_IDS].next_id),
+		.int_limits = {
+			.min	= SYSCTL_ZERO,
+			.max	= SYSCTL_INT_MAX,
+		},
 	},
 	{
 		.procname	= "msg_next_id",
-		.data		= &init_ipc_ns.ids[IPC_MSG_IDS].next_id,
-		.maxlen		= sizeof(init_ipc_ns.ids[IPC_MSG_IDS].next_id),
 		.mode		= 0444,
-		.proc_handler	= proc_dointvec_minmax,
-		.extra1		= SYSCTL_ZERO,
-		.extra2		= SYSCTL_INT_MAX,
+		.type		= SYSCTL_FIELD_INT,
+		.data_offset	= SYSCTL_FIELD_INT_OFFSET(struct ipc_namespace,
+							  ids[IPC_MSG_IDS].next_id),
+		.int_limits = {
+			.min	= SYSCTL_ZERO,
+			.max	= SYSCTL_INT_MAX,
+		},
 	},
 	{
 		.procname	= "shm_next_id",
-		.data		= &init_ipc_ns.ids[IPC_SHM_IDS].next_id,
-		.maxlen		= sizeof(init_ipc_ns.ids[IPC_SHM_IDS].next_id),
 		.mode		= 0444,
-		.proc_handler	= proc_dointvec_minmax,
-		.extra1		= SYSCTL_ZERO,
-		.extra2		= SYSCTL_INT_MAX,
+		.type		= SYSCTL_FIELD_INT,
+		.data_offset	= SYSCTL_FIELD_INT_OFFSET(struct ipc_namespace,
+							  ids[IPC_SHM_IDS].next_id),
+		.int_limits = {
+			.min	= SYSCTL_ZERO,
+			.max	= SYSCTL_INT_MAX,
+		},
 	},
 #endif
 };
@@ -244,57 +265,16 @@ static struct ctl_table_root set_root = {
 
 bool setup_ipc_sysctls(struct ipc_namespace *ns)
 {
-	struct ctl_table *tbl;
+	struct sysctl_context ctx = {
+		.type = SYSCTL_CONTEXT_IPC_NS,
+		.object_size = sizeof(*ns),
+		.ns.ipc_ns = ns,
+	};
 
 	setup_sysctl_set(&ns->ipc_set, &set_root, set_is_seen);
-
-	tbl = kmemdup(ipc_sysctls, sizeof(ipc_sysctls), GFP_KERNEL);
-	if (tbl) {
-		int i;
-
-		for (i = 0; i < ARRAY_SIZE(ipc_sysctls); i++) {
-			if (tbl[i].data == &init_ipc_ns.shm_ctlmax)
-				tbl[i].data = &ns->shm_ctlmax;
-
-			else if (tbl[i].data == &init_ipc_ns.shm_ctlall)
-				tbl[i].data = &ns->shm_ctlall;
-
-			else if (tbl[i].data == &init_ipc_ns.shm_ctlmni)
-				tbl[i].data = &ns->shm_ctlmni;
-
-			else if (tbl[i].data == &init_ipc_ns.shm_rmid_forced)
-				tbl[i].data = &ns->shm_rmid_forced;
-
-			else if (tbl[i].data == &init_ipc_ns.msg_ctlmax)
-				tbl[i].data = &ns->msg_ctlmax;
-
-			else if (tbl[i].data == &init_ipc_ns.msg_ctlmni)
-				tbl[i].data = &ns->msg_ctlmni;
-
-			else if (tbl[i].data == &init_ipc_ns.msg_ctlmnb)
-				tbl[i].data = &ns->msg_ctlmnb;
-
-			else if (tbl[i].data == &init_ipc_ns.sem_ctls)
-				tbl[i].data = &ns->sem_ctls;
-#ifdef CONFIG_CHECKPOINT_RESTORE
-			else if (tbl[i].data == &init_ipc_ns.ids[IPC_SEM_IDS].next_id)
-				tbl[i].data = &ns->ids[IPC_SEM_IDS].next_id;
-
-			else if (tbl[i].data == &init_ipc_ns.ids[IPC_MSG_IDS].next_id)
-				tbl[i].data = &ns->ids[IPC_MSG_IDS].next_id;
-
-			else if (tbl[i].data == &init_ipc_ns.ids[IPC_SHM_IDS].next_id)
-				tbl[i].data = &ns->ids[IPC_SHM_IDS].next_id;
-#endif
-			else
-				tbl[i].data = NULL;
-		}
-
-		ns->ipc_sysctls = __register_sysctl_table(&ns->ipc_set, "kernel", tbl,
-							  ARRAY_SIZE(ipc_sysctls));
-	}
+	ns->ipc_sysctls = register_sysctl_fields(&ns->ipc_set, "kernel",
+						 ipc_sysctls, &ctx);
 	if (!ns->ipc_sysctls) {
-		kfree(tbl);
 		retire_sysctl_set(&ns->ipc_set);
 		return false;
 	}
@@ -304,12 +284,8 @@ bool setup_ipc_sysctls(struct ipc_namespace *ns)
 
 void retire_ipc_sysctls(struct ipc_namespace *ns)
 {
-	const struct ctl_table *tbl;
-
-	tbl = ns->ipc_sysctls->ctl_table_arg;
 	unregister_sysctl_table(ns->ipc_sysctls);
 	retire_sysctl_set(&ns->ipc_set);
-	kfree(tbl);
 }
 
 static int __init ipc_sysctl_init(void)
@@ -326,6 +302,7 @@ device_initcall(ipc_sysctl_init);
 static int __init ipc_mni_extend(char *str)
 {
 	ipc_mni = IPCMNI_EXTEND;
+	ipc_mni_max = IPCMNI_EXTEND;
 	ipc_mni_shift = IPCMNI_EXTEND_SHIFT;
 	ipc_min_cycle = IPCMNI_EXTEND_MIN_CYCLE;
 	pr_info("IPCMNI extended to %d.\n", ipc_mni);
Signature · 1 line
2.55.0
#18 lore raw

PATCH v3 4/5 sysctl: mq: use typed fields for IPC namespace sysctls

In reply to Alexey Gladkov

Convert mq_sysctls. The table can now share one static array across ipc namespaces instead of allocating and rewriting a ctl_table copy for each registration.

---
 ipc/mq_sysctl.c | 104 ++++++++++++++++++++----------------------------
 1 file changed, 43 insertions(+), 61 deletions(-)
ipc/mq_sysctl.c +43 -61
index 0dd12e1c9f53..e237247d330b 100644
@@ -14,55 +14,63 @@
 #include <linux/slab.h>
 #include <linux/cred.h>
 
-static int msg_max_limit_min = MIN_MSGMAX;
-static int msg_max_limit_max = HARD_MSGMAX;
+static unsigned int msg_max_limit_min = MIN_MSGMAX;
+static unsigned int msg_max_limit_max = HARD_MSGMAX;
 
-static int msg_maxsize_limit_min = MIN_MSGSIZEMAX;
-static int msg_maxsize_limit_max = HARD_MSGSIZEMAX;
+static unsigned int msg_maxsize_limit_min = MIN_MSGSIZEMAX;
+static unsigned int msg_maxsize_limit_max = HARD_MSGSIZEMAX;
 
-static const struct ctl_table mq_sysctls[] = {
+static const struct sysctl_field mq_sysctls[] = {
 	{
 		.procname	= "queues_max",
-		.data		= &init_ipc_ns.mq_queues_max,
-		.maxlen		= sizeof(int),
 		.mode		= 0644,
-		.proc_handler	= proc_dointvec,
+		.type		= SYSCTL_FIELD_UINT,
+		.data_offset	= SYSCTL_FIELD_UINT_OFFSET(struct ipc_namespace,
+							   mq_queues_max),
 	},
 	{
 		.procname	= "msg_max",
-		.data		= &init_ipc_ns.mq_msg_max,
-		.maxlen		= sizeof(int),
 		.mode		= 0644,
-		.proc_handler	= proc_dointvec_minmax,
-		.extra1		= &msg_max_limit_min,
-		.extra2		= &msg_max_limit_max,
+		.type		= SYSCTL_FIELD_UINT,
+		.data_offset	= SYSCTL_FIELD_UINT_OFFSET(struct ipc_namespace,
+							   mq_msg_max),
+		.uint_limits = {
+			.min	= &msg_max_limit_min,
+			.max	= &msg_max_limit_max,
+		},
 	},
 	{
 		.procname	= "msgsize_max",
-		.data		= &init_ipc_ns.mq_msgsize_max,
-		.maxlen		= sizeof(int),
 		.mode		= 0644,
-		.proc_handler	= proc_dointvec_minmax,
-		.extra1		= &msg_maxsize_limit_min,
-		.extra2		= &msg_maxsize_limit_max,
+		.type		= SYSCTL_FIELD_UINT,
+		.data_offset	= SYSCTL_FIELD_UINT_OFFSET(struct ipc_namespace,
+							   mq_msgsize_max),
+		.uint_limits = {
+			.min	= &msg_maxsize_limit_min,
+			.max	= &msg_maxsize_limit_max,
+		},
 	},
 	{
 		.procname	= "msg_default",
-		.data		= &init_ipc_ns.mq_msg_default,
-		.maxlen		= sizeof(int),
 		.mode		= 0644,
-		.proc_handler	= proc_dointvec_minmax,
-		.extra1		= &msg_max_limit_min,
-		.extra2		= &msg_max_limit_max,
+		.type		= SYSCTL_FIELD_UINT,
+		.data_offset	= SYSCTL_FIELD_UINT_OFFSET(struct ipc_namespace,
+							   mq_msg_default),
+		.uint_limits = {
+			.min	= &msg_max_limit_min,
+			.max	= &msg_max_limit_max,
+		},
 	},
 	{
 		.procname	= "msgsize_default",
-		.data		= &init_ipc_ns.mq_msgsize_default,
-		.maxlen		= sizeof(int),
 		.mode		= 0644,
-		.proc_handler	= proc_dointvec_minmax,
-		.extra1		= &msg_maxsize_limit_min,
-		.extra2		= &msg_maxsize_limit_max,
+		.type		= SYSCTL_FIELD_UINT,
+		.data_offset	= SYSCTL_FIELD_UINT_OFFSET(struct ipc_namespace,
+							   mq_msgsize_default),
+		.uint_limits = {
+			.min	= &msg_maxsize_limit_min,
+			.max	= &msg_maxsize_limit_max,
+		},
 	},
 };
 
@@ -116,39 +124,17 @@ static struct ctl_table_root set_root = {
 
 bool setup_mq_sysctls(struct ipc_namespace *ns)
 {
-	struct ctl_table *tbl;
+	struct sysctl_context ctx = {
+		.type = SYSCTL_CONTEXT_IPC_NS,
+		.object_size = sizeof(*ns),
+		.ns.ipc_ns = ns,
+	};
 
 	setup_sysctl_set(&ns->mq_set, &set_root, set_is_seen);
 
-	tbl = kmemdup(mq_sysctls, sizeof(mq_sysctls), GFP_KERNEL);
-	if (tbl) {
-		int i;
-
-		for (i = 0; i < ARRAY_SIZE(mq_sysctls); i++) {
-			if (tbl[i].data == &init_ipc_ns.mq_queues_max)
-				tbl[i].data = &ns->mq_queues_max;
-
-			else if (tbl[i].data == &init_ipc_ns.mq_msg_max)
-				tbl[i].data = &ns->mq_msg_max;
-
-			else if (tbl[i].data == &init_ipc_ns.mq_msgsize_max)
-				tbl[i].data = &ns->mq_msgsize_max;
-
-			else if (tbl[i].data == &init_ipc_ns.mq_msg_default)
-				tbl[i].data = &ns->mq_msg_default;
-
-			else if (tbl[i].data == &init_ipc_ns.mq_msgsize_default)
-				tbl[i].data = &ns->mq_msgsize_default;
-			else
-				tbl[i].data = NULL;
-		}
-
-		ns->mq_sysctls = __register_sysctl_table(&ns->mq_set,
-							 "fs/mqueue", tbl,
-							 ARRAY_SIZE(mq_sysctls));
-	}
+	ns->mq_sysctls = register_sysctl_fields(&ns->mq_set, "fs/mqueue",
+						mq_sysctls, &ctx);
 	if (!ns->mq_sysctls) {
-		kfree(tbl);
 		retire_sysctl_set(&ns->mq_set);
 		return false;
 	}
@@ -158,10 +144,6 @@ bool setup_mq_sysctls(struct ipc_namespace *ns)
 
 void retire_mq_sysctls(struct ipc_namespace *ns)
 {
-	const struct ctl_table *tbl;
-
-	tbl = ns->mq_sysctls->ctl_table_arg;
 	unregister_sysctl_table(ns->mq_sysctls);
 	retire_sysctl_set(&ns->mq_set);
-	kfree(tbl);
 }
Signature · 1 line
2.55.0
#19 lore raw

PATCH v3 5/5 sysctl: use typed fields for ucount limits

In reply to Alexey Gladkov

User namespace sysctl registration clones the entire ctl_table and assigns ucount_max entries by table index. This allocates a table for every user namespace and makes the data mapping depend on the descriptor order.

Use typed field offsets to associate each entry explicitly with its ucount_max element. The static descriptor array can then be shared by all user namespaces without allocating or rewriting a ctl_table copy.

---
 kernel/ucount.c | 64 +++++++++++++++++++++++--------------------------
 1 file changed, 30 insertions(+), 34 deletions(-)
kernel/ucount.c +30 -34
index ec8b1445e287..e85aecc3b6b3 100644
@@ -64,34 +64,38 @@ static struct ctl_table_root set_root = {
 static long ue_zero = 0;
 static long ue_int_max = INT_MAX;
 
-#define UCOUNT_ENTRY(name)					\
+#define UCOUNT_ENTRY(name, ucount)				\
 	{							\
 		.procname	= name,				\
-		.maxlen		= sizeof(long),			\
 		.mode		= 0644,				\
-		.proc_handler	= proc_doulongvec_minmax,	\
-		.extra1		= &ue_zero,			\
-		.extra2		= &ue_int_max,			\
+		.type		= SYSCTL_FIELD_LONG,		\
+		.data_offset	= SYSCTL_FIELD_LONG_OFFSET(struct user_namespace, \
+							   ucount_max[ucount]),   \
+		.long_limits	= {				\
+			.min	= &ue_zero,			\
+			.max	= &ue_int_max,			\
+		}						\
 	}
-static const struct ctl_table user_table[] = {
-	UCOUNT_ENTRY("max_user_namespaces"),
-	UCOUNT_ENTRY("max_pid_namespaces"),
-	UCOUNT_ENTRY("max_uts_namespaces"),
-	UCOUNT_ENTRY("max_ipc_namespaces"),
-	UCOUNT_ENTRY("max_net_namespaces"),
-	UCOUNT_ENTRY("max_mnt_namespaces"),
-	UCOUNT_ENTRY("max_cgroup_namespaces"),
-	UCOUNT_ENTRY("max_time_namespaces"),
+static const struct sysctl_field user_table[] = {
+	UCOUNT_ENTRY("max_user_namespaces",	UCOUNT_USER_NAMESPACES),
+	UCOUNT_ENTRY("max_pid_namespaces",	UCOUNT_PID_NAMESPACES),
+	UCOUNT_ENTRY("max_uts_namespaces",	UCOUNT_UTS_NAMESPACES),
+	UCOUNT_ENTRY("max_ipc_namespaces",	UCOUNT_IPC_NAMESPACES),
+	UCOUNT_ENTRY("max_net_namespaces",	UCOUNT_NET_NAMESPACES),
+	UCOUNT_ENTRY("max_mnt_namespaces",	UCOUNT_MNT_NAMESPACES),
+	UCOUNT_ENTRY("max_cgroup_namespaces",	UCOUNT_CGROUP_NAMESPACES),
+	UCOUNT_ENTRY("max_time_namespaces",	UCOUNT_TIME_NAMESPACES),
 #ifdef CONFIG_INOTIFY_USER
-	UCOUNT_ENTRY("max_inotify_instances"),
-	UCOUNT_ENTRY("max_inotify_watches"),
+	UCOUNT_ENTRY("max_inotify_instances",	UCOUNT_INOTIFY_INSTANCES),
+	UCOUNT_ENTRY("max_inotify_watches",	UCOUNT_INOTIFY_WATCHES),
 #endif
 #ifdef CONFIG_FANOTIFY
-	UCOUNT_ENTRY("max_fanotify_groups"),
-	UCOUNT_ENTRY("max_fanotify_marks"),
+	UCOUNT_ENTRY("max_fanotify_groups",	UCOUNT_FANOTIFY_GROUPS),
+	UCOUNT_ENTRY("max_fanotify_marks",	UCOUNT_FANOTIFY_MARKS),
 #endif
 #if IS_ENABLED(CONFIG_BINFMT_MISC)
-	UCOUNT_ENTRY("max_binfmt_misc_interpreters"),
+	UCOUNT_ENTRY("max_binfmt_misc_interpreters",
+		     UCOUNT_BINFMT_MISC_INTERPRETERS),
 #endif
 };
 #endif /* CONFIG_SYSCTL */
@@ -99,21 +103,17 @@ static const struct ctl_table user_table[] = {
 bool setup_userns_sysctls(struct user_namespace *ns)
 {
 #ifdef CONFIG_SYSCTL
-	struct ctl_table *tbl;
+	struct sysctl_context ctx = {
+		.type = SYSCTL_CONTEXT_USER_NS,
+		.object_size = sizeof(*ns),
+		.ns.user_ns = ns,
+	};
 
 	BUILD_BUG_ON(ARRAY_SIZE(user_table) != UCOUNT_COUNTS);
 	setup_sysctl_set(&ns->set, &set_root, set_is_seen);
-	tbl = kmemdup(user_table, sizeof(user_table), GFP_KERNEL);
-	if (tbl) {
-		int i;
-		for (i = 0; i < UCOUNT_COUNTS; i++) {
-			tbl[i].data = &ns->ucount_max[i];
-		}
-		ns->sysctls = __register_sysctl_table(&ns->set, "user", tbl,
-						      ARRAY_SIZE(user_table));
-	}
+	ns->sysctls = register_sysctl_fields(&ns->set, "user",
+					     user_table, &ctx);
 	if (!ns->sysctls) {
-		kfree(tbl);
 		retire_sysctl_set(&ns->set);
 		return false;
 	}
@@ -124,12 +124,8 @@ bool setup_userns_sysctls(struct user_namespace *ns)
 void retire_userns_sysctls(struct user_namespace *ns)
 {
 #ifdef CONFIG_SYSCTL
-	const struct ctl_table *tbl;
-
-	tbl = ns->sysctls->ctl_table_arg;
 	unregister_sysctl_table(ns->sysctls);
 	retire_sysctl_set(&ns->set);
-	kfree(tbl);
 #endif
 }
 
Signature · 1 line
2.55.0
#20 lore raw
In reply to Alexey Gladkov
Quoted 40 lines · Alexey Gladkov

Hi,

Some sysctl users copy an otherwise static ctl_table for each namespace or device, then replace data and limit pointers before registration. Besides the per-instance allocation, these fixups often depend on table indices:
reordering an entry without updating the fixup can silently bind it to the wrong data or limits.

This series lets such users describe a table once with static, read-only sysctl_field entries. Each field names a data type and an offset into an object selected by a sysctl_context at registration time. The offset helpers check the member type at build time. Limits and custom handlers remain optional properties of a field; the field type does not encode a choice of handler or whether limits are present. A subsystem that needs more than a namespace can embed sysctl_context in its own registration context.

The sysctl core keeps the context with the table header and materializes a ctl_table entry when an existing handler, permission check or BPF hook needs one. Legacy ctl_table registrations retain their own path, and subsystems can convert tables independently. The IPC, mqueue and ucount conversions here remove table copies without changing their backing data.

Changes since v2:

- Drop sysctl_uint_vals, as requested by Joel. Existing limit constants suffice for these conversions.
- Drop the *_MINMAX field types. A field's type now describes its data, while optional min/max pointers supply bounds to the selected handler.
- Keep __register_sysctl_table() and __register_sysctl_fields() as separate registration paths, sharing only allocation and tree insertion helpers.
- Address the sashiko findings: avoid calling mode_fn under sysctl_lock, validate the effective maxlen against the context object, and reject NO_DATA fields paired with standard handlers that require data.

This is the first part of the conversion. Network sysctls and other subsystem-specific users will follow in separate series for review by their maintainers.

Quoted 6 lines

Alexey Gladkov (5):
proc: sysctl: address table entries by index
sysctl: add typed field descriptors
sysctl: ipc: use typed fields for IPC namespace sysctls
sysctl: mq: use typed fields for IPC namespace sysctls
sysctl: use typed fields for ucount limits

Gentle reminder about the patchset.

Quoted 14 lines
 fs/proc/inode.c        |   2 +-
 fs/proc/internal.h     |   2 +-
 fs/proc/proc_sysctl.c  | 681 +++++++++++++++++++++++++++++------------
 include/linux/sysctl.h | 138 ++++++++-
 ipc/ipc_sysctl.c       | 189 +++++-------
 ipc/mq_sysctl.c        | 104 +++----
 kernel/ucount.c        |  64 ++--
 7 files changed, 772 insertions(+), 408 deletions(-)

base-commit: 587858367581b9c55c3690f4e63382ad622719d4

Signature · 1 line
2.55.0
Signature · 1 line
Rgrds, legion
maintainer
#21 lore raw
Quoted 41 lines · Alexey Gladkov
Quoted 40 lines · Alexey Gladkov

Hi,

Some sysctl users copy an otherwise static ctl_table for each namespace or device, then replace data and limit pointers before registration. Besides the per-instance allocation, these fixups often depend on table indices:
reordering an entry without updating the fixup can silently bind it to the wrong data or limits.

This series lets such users describe a table once with static, read-only sysctl_field entries. Each field names a data type and an offset into an object selected by a sysctl_context at registration time. The offset helpers check the member type at build time. Limits and custom handlers remain optional properties of a field; the field type does not encode a choice of handler or whether limits are present. A subsystem that needs more than a namespace can embed sysctl_context in its own registration context.

The sysctl core keeps the context with the table header and materializes a ctl_table entry when an existing handler, permission check or BPF hook needs one. Legacy ctl_table registrations retain their own path, and subsystems can convert tables independently. The IPC, mqueue and ucount conversions here remove table copies without changing their backing data.

Changes since v2:

- Drop sysctl_uint_vals, as requested by Joel. Existing limit constants suffice for these conversions.
- Drop the *_MINMAX field types. A field's type now describes its data, while optional min/max pointers supply bounds to the selected handler.
- Keep __register_sysctl_table() and __register_sysctl_fields() as separate registration paths, sharing only allocation and tree insertion helpers.
- Address the sashiko findings: avoid calling mode_fn under sysctl_lock, validate the effective maxlen against the context object, and reject NO_DATA fields paired with standard handlers that require data.

This is the first part of the conversion. Network sysctls and other subsystem-specific users will follow in separate series for review by their maintainers.

Quoted 8 lines
Quoted 6 lines

Alexey Gladkov (5):
proc: sysctl: address table entries by index
sysctl: add typed field descriptors
sysctl: ipc: use typed fields for IPC namespace sysctls
sysctl: mq: use typed fields for IPC namespace sysctls
sysctl: use typed fields for ucount limits

Gentle reminder about the patchset.

I have not forgotten. It's on my (very large after PTO) TODO list :) thx for the reminder.

best

Quoted 19 lines
Quoted 15 lines
 fs/proc/inode.c        |   2 +-
 fs/proc/internal.h     |   2 +-
 fs/proc/proc_sysctl.c  | 681 +++++++++++++++++++++++++++++------------
 include/linux/sysctl.h | 138 ++++++++-
 ipc/ipc_sysctl.c       | 189 +++++-------
 ipc/mq_sysctl.c        | 104 +++----
 kernel/ucount.c        |  64 ++--
 7 files changed, 772 insertions(+), 408 deletions(-)

base-commit: 587858367581b9c55c3690f4e63382ad622719d4

Signature · 1 line
2.55.0
Signature · 1 line
Rgrds, legion
maintainer
#22 lore raw
In reply to Alexey Gladkov
Quoted 41 lines · Alexey Gladkov
Quoted 40 lines · Alexey Gladkov

Hi,

Some sysctl users copy an otherwise static ctl_table for each namespace or device, then replace data and limit pointers before registration. Besides the per-instance allocation, these fixups often depend on table indices:
reordering an entry without updating the fixup can silently bind it to the wrong data or limits.

This series lets such users describe a table once with static, read-only sysctl_field entries. Each field names a data type and an offset into an object selected by a sysctl_context at registration time. The offset helpers check the member type at build time. Limits and custom handlers remain optional properties of a field; the field type does not encode a choice of handler or whether limits are present. A subsystem that needs more than a namespace can embed sysctl_context in its own registration context.

The sysctl core keeps the context with the table header and materializes a ctl_table entry when an existing handler, permission check or BPF hook needs one. Legacy ctl_table registrations retain their own path, and subsystems can convert tables independently. The IPC, mqueue and ucount conversions here remove table copies without changing their backing data.

Changes since v2:

- Drop sysctl_uint_vals, as requested by Joel. Existing limit constants suffice for these conversions.
- Drop the *_MINMAX field types. A field's type now describes its data, while optional min/max pointers supply bounds to the selected handler.
- Keep __register_sysctl_table() and __register_sysctl_fields() as separate registration paths, sharing only allocation and tree insertion helpers.
- Address the sashiko findings: avoid calling mode_fn under sysctl_lock, validate the effective maxlen against the context object, and reject NO_DATA fields paired with standard handlers that require data.

This is the first part of the conversion. Network sysctls and other subsystem-specific users will follow in separate series for review by their maintainers.

Quoted 8 lines
Quoted 6 lines

Alexey Gladkov (5):
proc: sysctl: address table entries by index
sysctl: add typed field descriptors
sysctl: ipc: use typed fields for IPC namespace sysctls
sysctl: mq: use typed fields for IPC namespace sysctls
sysctl: use typed fields for ucount limits

Gentle reminder about the patchset.

Thx for persisting and sorry for the late reply.

The more I look at this, the more I am convinced that the first problem that we should address is having a .data, .extra1 and .extra2 as void pointers. Once that is out of the way, then we can focus on getting rid of the kmemdup of the ctl_tables.

Motivation
==========

Here are the motivational points for this series:
1. Remove the per-instance allocation thereby reducing the memory footprint
* This one is relevant for instances with lots of NSs.

2. Remove the possibility of mistakenly associating one variable with a different sysctl name (because of un-synced changes)
* This one is the least important for me. Keeping it synced is a little scary but something that I would be willing to live with.

3. Type checking .data, .extra1 and .extra2. "unambiguous pointer" that answers what it is.
* I think if we are serious about type checking, we need to fix the existing (potentially uneeded) void pointers (.data, extra{1,2}). Which is not an easy as there are callers that (mis)use these in different ways. This has been discussed before (in [1,2,3] among others)

4. Don't bleed caller structs/types/logic into sysctl.
* This one is mine and is the reason I really don't want it done in the manner that you propose. IMO, having network (or other callers logic) inside sysctl makes no sense.

I'll come back to these series after having yet another look at the void pointers in ctl_table.

Best

[1] https://lore.kernel.org/all/[email protected]
[2] https://lore.kernel.org/all/[email protected]
[3] https://lore.kernel.org/all/[email protected]

#23 lore raw
Quoted 78 lines · Joel Granados
Quoted 41 lines · Alexey Gladkov
Quoted 40 lines · Alexey Gladkov

Hi,

Some sysctl users copy an otherwise static ctl_table for each namespace or device, then replace data and limit pointers before registration. Besides the per-instance allocation, these fixups often depend on table indices:
reordering an entry without updating the fixup can silently bind it to the wrong data or limits.

This series lets such users describe a table once with static, read-only sysctl_field entries. Each field names a data type and an offset into an object selected by a sysctl_context at registration time. The offset helpers check the member type at build time. Limits and custom handlers remain optional properties of a field; the field type does not encode a choice of handler or whether limits are present. A subsystem that needs more than a namespace can embed sysctl_context in its own registration context.

The sysctl core keeps the context with the table header and materializes a ctl_table entry when an existing handler, permission check or BPF hook needs one. Legacy ctl_table registrations retain their own path, and subsystems can convert tables independently. The IPC, mqueue and ucount conversions here remove table copies without changing their backing data.

Changes since v2:

- Drop sysctl_uint_vals, as requested by Joel. Existing limit constants suffice for these conversions.
- Drop the *_MINMAX field types. A field's type now describes its data, while optional min/max pointers supply bounds to the selected handler.
- Keep __register_sysctl_table() and __register_sysctl_fields() as separate registration paths, sharing only allocation and tree insertion helpers.
- Address the sashiko findings: avoid calling mode_fn under sysctl_lock, validate the effective maxlen against the context object, and reject NO_DATA fields paired with standard handlers that require data.

This is the first part of the conversion. Network sysctls and other subsystem-specific users will follow in separate series for review by their maintainers.

Quoted 8 lines
Quoted 6 lines

Alexey Gladkov (5):
proc: sysctl: address table entries by index
sysctl: add typed field descriptors
sysctl: ipc: use typed fields for IPC namespace sysctls
sysctl: mq: use typed fields for IPC namespace sysctls
sysctl: use typed fields for ucount limits

Gentle reminder about the patchset.

Thx for persisting and sorry for the late reply.

The more I look at this, the more I am convinced that the first problem that we should address is having a .data, .extra1 and .extra2 as void pointers. Once that is out of the way, then we can focus on getting rid of the kmemdup of the ctl_tables.

Motivation
==========

Here are the motivational points for this series:
1. Remove the per-instance allocation thereby reducing the memory footprint
* This one is relevant for instances with lots of NSs.

2. Remove the possibility of mistakenly associating one variable with a different sysctl name (because of un-synced changes)
* This one is the least important for me. Keeping it synced is a little scary but something that I would be willing to live with.

3. Type checking .data, .extra1 and .extra2. "unambiguous pointer" that answers what it is.
* I think if we are serious about type checking, we need to fix the existing (potentially uneeded) void pointers (.data, extra{1,2}). Which is not an easy as there are callers that (mis)use these in different ways. This has been discussed before (in [1,2,3] among others)

Quoted 7 lines

4. Don't bleed caller structs/types/logic into sysctl.
* This one is mine and is the reason I really don't want it done in the manner that you propose. IMO, having network (or other callers logic) inside sysctl makes no sense.

I'll come back to these series after having yet another look at the void pointers in ctl_table.

In that case, I'll step back with my patches for now.

Thanks for review.

Quoted 7 lines

Best

[1] https://lore.kernel.org/all/[email protected]
[2] https://lore.kernel.org/all/[email protected]
[3] https://lore.kernel.org/all/[email protected]

Signature · 1 line
Rgrds, legion