mirror of https://lore.kernel.org/lkml/
 help / color / mirror / Atom feed
From: Wen Yang <wen.yang@linux.dev>
To: David Laight <david.laight.linux@gmail.com>
Cc: Joel Granados <joel.granados@kernel.org>,
	linux-fsdevel@vger.kernel.org, linux-kernel@vger.kernel.org
Subject: Re: [RFC PATCH 00/39] sysctl: replace overloaded extra pointers with typed bounds and context
Date: Thu, 5 Nov 2026 02:32:19 +0800	[thread overview]
Message-ID: <01b45745-38e9-48b3-9ac5-b62e6d57db2b@linux.dev> (raw)
In-Reply-To: <20261102165342.79fc1580@pumpkin>



On 11/3/26 00:54, David Laight wrote:
> On Mon, 2 Nov 2026 23:40:40 +0800
> Wen Yang <wen.yang@linux.dev> wrote:
> 
>> On 10/29/26 18:58, David Laight wrote:
>>> On Thu, 29 Oct 2026 00:34:39 +0800
>>> wen.yang@linux.dev wrote:
>>>    
>>>> From: Wen Yang <wen.yang@linux.dev>
>>>>
>>>> struct ctl_table describes an entry with data, maxlen and, until this
>>>> series, two untyped misc pointers in extra1 and extra2. Those two pointers
>>>> have accumulated at least seven unrelated jobs: a fixed bound, a bound read
>>>> through a pointer on every call, an object or namespace pointer, a small
>>>> integer cookie encoded into the pointer, a lock signal, NULL standing for
>>>> "no bound", and a value the core itself casts to unsigned int while
>>>> validating a u8 range. Nothing in the entry says which job applies, so
>>>> readers infer it from the handler, and the core gets it wrong where it
>>>> treats extra1 as an unsigned int.
>>>>
>>>> The numeric half of this change is the direction Eric Biederman asked for:
>>>>
>>>>     You can still save a lot more by turning .extra1 and .extra2
>>>>     into longs instead of keeping them as pointers and needing
>>>>     constants to be pointed at somewhere.
>>>>
>>>>     [...] I don't see any reason why you want shared
>>>>     constants for such a handful of things.  Especially when
>>>>     it has proven to be error prone in the past.
>>>>
>>>> min/max are plain unsigned long members, so the comparisons are ordinary
>>>> C and cannot be confused with a pointer. The shared const arrays that only
>>>> existed to give those pointers an address (sysctl_vals and
>>>> sysctl_long_vals) are removed as well.
>>>>
>>>> The tree already records this direction. fs/proc/proc_sysctl.c documents
>>>> extra1/extra2 as "extra pointers usable by the proc handler routines" and
>>>> carries:
>>>>
>>>>     XXX: we should eventually modify these to use long min / max [0]
>>>>     [0] https://lkml.kernel.org/87zgpte9o4.fsf@email.froward.int.ebiederm.org
>>>>
>>>> This series completes that TODO and removes the comment.
>>>>
>>>> priv is this series' addition: the same pair of fields also hides handler
>>>> context, which a numeric bound cannot express, and keeping it out of data
>>>> is what removes the restore-before-delegate pattern.
>>>>
>>>> The overloading is also a memory-safety hazard. The obvious alternative --
>>>> keep handler context in data and restore the value pointer in each handler
>>>> -- needs an original_data field in nine addrconf and two devinet handlers;
>>>> one missed restore turns a user-supplied integer into a write over a kernel
>>>> pointer. nf_log reaches the same problem from the other side: it replaces
>>>> data with a local buffer inside a single handler, so data means two things
>>>> within one function. This revision removes that temptation instead of
>>>> managing it.
>>>>
>>>> The core is not exempt. sysctl_check_table_array() reads
>>>> *(unsigned int *)table->extra1 to validate a u8 range. That is only correct
>>>> while every u8 entry happens to point extra1/extra2 at an int or unsigned
>>>> int; a long bound, a context pointer or an encoded integer would be read as
>>>> four bytes of the wrong object. No such entry exists today, so this is a
>>>> latent class rather than a live bug, but the invariant is untyped and
>>>> unchecked, and nf_log already encodes an integer into extra1.
>>>>
>>>> The refactor is the first five patches; they define the replacement
>>>> contract and prove it covers every entry shape the tree uses today.
>>>>
>>>>     1. min/max hold numeric bounds by value, and priv holds handler context.
>>>>        The range converters read min/max whenever extra1 and extra2 are
>>>>        clear and keep reading the pointers while they are set, so existing
>>>>        callers keep working while they migrate and no parallel handler API or
>>>>        renamed symbol is introduced. Signed handlers read the bounds as
>>>>        signed longs, an absent endpoint is the type limit rather than NULL so
>>>>        zero stays a real bound, and a run-time range is refreshed before
>>>>        every call. Registration rejects priv on a standard handler.
>>>>
>>>>     2. The debug test module is converted to numeric bounds as the first
>>>>        user.
>>>>
>>>>     3. KUnit coverage for numeric bounds and the handlers that consume them:
>>>>        full type ranges, one-sided and zero bounds, entries without backing
>>>>        storage, explicit handlers for arrays and members, and rejection of
>>>>        writes that would partially commit.
>>>>
>>>>     4. KUnit coverage for run-time ranges across the scalar widths and
>>>>        jiffies, run-time bound pointers, sibling bounds, typed context, owner
>>>>        derivation, bool, registration lifecycles, and simulated per-net
>>>>        duplication.
>>>>
>>>>     5. KUnit coverage for priv: a standard handler must ignore it,
>>>>        registration must reject it with a standard handler, and
>>>>        re-registration must keep both the value pointer and the context. The
>>>>        same patch moves the debug module's dynamic-value entry to the
>>>>        data/priv shape.
>>>>
>>>> On the CTLTBL_ENTRY_* constructors: I kept the naming and argument order you
>>>> suggested while reworking the series. As it turned out, no production table
>>>> needed them -- the converted entries read naturally as designated
>>>> initializers -- so I have not introduced them as a public API in this
>>>> version; the KUnit file keeps an equivalent constructor privately, so the
>>>> tests stay readable. I still think the public version is worth doing, and I
>>>> would be happy to send it as a follow-up once there are real users, so the
>>>> API and its callers can be reviewed together.
>>>>
>>>> Everything after patch 5 is migration and adds no new contract. Patches
>>>> 6-32 convert the remaining users and derive owners from their backing
>>>> storage. Patches 33-38 move handler context onto priv, one subsystem per
>>>> patch; each of those files is touched exactly once and goes straight from
>>>> the old pointer field to priv, with no intermediate data-as-context step.
>>>> Patch 39 removes the extra pointers and the compatibility read.
>>>>
>>>> The parport conversion illustrates a concrete problem with the overloaded
>>>> fields. In the base tree, registration overwrites the spintime minimum
>>>> pointer in vars[0].extra1 with a port pointer. The range handler can then
>>>> check a write against data from struct parport instead of the intended
>>>> minimum. Moving the port context to priv and using numeric bounds also
>>>> corrects this existing bug. A separate fix can address it independently;
>>>> this RFC does not depend on that fix.
>>>>
>>>> Entries that used a *_minmax handler with neither extra pointer relied on
>>>> NULL meaning "no bound". They now state their type limits explicitly to
>>>> preserve that behaviour when the pointer path disappears. These include
>>>> aio-max-nr, acpi_video_flags and nmi_wd_lpm_factor.
>>>>
>>>> Owners that can still be recovered from data are deliberately left alone:
>>>> ipvs, neighbour, the fragment reassembly handlers, vsock, mptcp, rds and
>>>> smc keep using container_of() on their backing storage. priv is only for
>>>> entries where no such derivation exists. Mixed placement in data and priv
>>>> was considered and rejected because it recreates the guessing problem this
>>>> series removes.
>>>>
>>>> Relative to the base tree, the final state removes all 841 extra1/extra2
>>>> assignments across 123 files, removes the shared sysctl_vals and
>>>> sysctl_long_vals arrays (21 references), and states bounds at each entry
>>>> (~574 new min/max initializers) instead of pointing at constants. The
>>>> sysctl ABI is unchanged; the parport spintime minimum is corrected as
>>>> described above.
>>>>
>>>> The cost is 8 bytes per ctl_table entry on 64-bit (56 -> 64) and 4 bytes on
>>>> 32-bit, because priv is appended; the range read path gains no indirection
>>>> and the migrated entries no longer dereference a bound pointer.
>>>
>>> Can you remove the little-used 'priv' parameter by making the extra
>>> information just follow the table entry (nested structure) and have
>>> the relevant functions use container_of() to get the outer structure
>>> and extra values?
>>>    
>>
>>
>> Hi David, Hi Joel,
>>
>> I reworked the context side of the series so that priv is gone entirely;
>> and the final tree has no per-entry context pointer.
>> A small helper recovers the enclosing object, but it needs the slot
>> index from the caller:
>>
>>     #define sysctl_table_container(entry, type, member, index) ({       \
>>             static_assert(__same_type(*(entry),                         \
>>                                       typeof_member(type, member[0])),  \
>>                           "pointer type mismatch");                     \
>>             (type *)((void *)(entry) - offsetof(type, member) -         \
>>                      (size_t)(index) * sizeof(*(entry)));               \
>>     })
>>
>> The reason plain container_of() is not enough is that the core keeps the
>> table as a contiguous array.  For
>>
>>     struct addrconf_sysctl_table {
>>             struct addrconf_sysctl_ctx context;
>>             struct ctl_table table[];
>>     };
>>
>> the handler receives an interior entry, and
>>
>>     container_of(ctl, struct addrconf_sysctl_table, table)
>>         == (char *)outer + index * sizeof(struct ctl_table)
>>
>> for ctl == &outer->table[index].  It only returns the outer object for
>> index 0.  Passing the index explicitly keeps the original registered
>> entry usable:
>>
>>     static int addrconf_sysctl_forward(const struct ctl_table *ctl, ...)
>>     {
>>             struct addrconf_sysctl_table *t =
>>                     sysctl_table_container(ctl, struct addrconf_sysctl_table,
>>                                            table,
>> ADDRCONF_SYSCTL_FORWARDING);
>>             const struct addrconf_sysctl_ctx *ctx = &t->context;
>>             ...
>>     }
>>
>> Shared handlers get one wrapper per slot; the common body does not need
>> to know its position:
>>
>>     #define DEVINET_CONF_WRAPPER(attr)                                  \
>>     static int devinet_conf_##attr(const struct ctl_table *ctl, ...)    \
>>     {                                                                   \
>>             struct devinet_sysctl_table *t =                            \
>>                     sysctl_table_container(ctl,                         \
>>                             struct devinet_sysctl_table, devinet_vars,  \
>>                             IPV4_DEVCONF_##attr - 1);                   \
>>             return devinet_conf_proc_common(ctl, &t->context, ...);     \
>>     }
>>
>> Single-entry tables simply use slot 0:
>>
>>     struct devinet_forward_table *t =
>>             sysctl_table_container(ctl, struct devinet_forward_table,
>> table, 0);
>>
>>
>> The series is 20 commits over 134 files, with no priv at any point.  If
>> you would prefer a different shape for the helper or the wrappers, I am
>> happy to adjust it.
> 
> Certainly worth thinking about for a followup patch.
> 
> A different option would be to pass the array element size when adding
> the items.
> Most code would just pass an array of 'struct ctl_table' but instead
> you could pass an array of a larger structure that started with a
> struct ctl_table.
> That should let you use container_of().
> That might even make some of these patches shorter/simpler (I just looked
> at #33 (ipv6 addresses) and it is somewhat convoluted.
> 

Hi David,

Thanks for the great feedback and suggestion.

Passing the array element size so that callers can embed struct
ctl_table at offset 0 and use plain container_of() is indeed a
very clean idea, and definitely worth exploring as a follow-up.

For the current bounds refactoring and priv removal, we will send
out v2 shortly for review.

Thanks,
Wen

      reply	other threads:[~2026-11-04 18:32 UTC|newest]

Thread overview: 45+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-10-28 16:34 wen.yang
2026-10-28 16:34 ` [RFC PATCH 01/39] " wen.yang
2026-10-28 21:09   ` David Laight
2026-10-28 16:34 ` [RFC PATCH 02/39] test_sysctl: convert bounds to numeric values wen.yang
2026-10-28 16:34 ` [RFC PATCH 03/39] sysctl: add KUnit coverage for numeric bounds and handlers wen.yang
2026-10-28 16:34 ` [RFC PATCH 04/39] sysctl: cover runtime ranges and context lifecycles in KUnit wen.yang
2026-10-28 16:34 ` [RFC PATCH 05/39] sysctl: cover priv in KUnit and the debug test module wen.yang
2026-10-28 16:34 ` [RFC PATCH 06/39] sysctl: convert core table bounds to numeric values wen.yang
2026-10-28 16:34 ` [RFC PATCH 07/39] sysctl: store architecture sysctl bounds by value wen.yang
2026-10-28 16:34 ` [RFC PATCH 08/39] sysctl: store crypto " wen.yang
2026-10-28 16:34 ` [RFC PATCH 09/39] sysctl: store device " wen.yang
2026-10-28 16:34 ` [RFC PATCH 10/39] sysctl: store filesystem " wen.yang
2026-10-28 16:34 ` [RFC PATCH 11/39] sysctl: store message-queue " wen.yang
2026-10-28 16:34 ` [RFC PATCH 12/39] sysctl: store kernel " wen.yang
2026-10-28 16:34 ` [RFC PATCH 13/39] sysctl: store memory-management " wen.yang
2026-10-28 16:34 ` [RFC PATCH 14/39] sysctl: store IPv6 scalar " wen.yang
2026-10-28 16:34 ` [RFC PATCH 15/39] sysctl: store netfilter " wen.yang
2026-10-28 16:34 ` [RFC PATCH 16/39] sysctl: store RPC " wen.yang
2026-10-28 16:34 ` [RFC PATCH 17/39] sysctl: store remaining network " wen.yang
2026-10-28 16:34 ` [RFC PATCH 18/39] sysctl: store security " wen.yang
2026-10-28 16:34 ` [RFC PATCH 19/39] io_uring: store its " wen.yang
2026-10-28 16:34 ` [RFC PATCH 20/39] sysctl: refresh dynamic scheduler and PID bounds per call wen.yang
2026-10-28 16:35 ` [RFC PATCH 21/39] sysctl: refresh device-dependent " wen.yang
2026-10-28 16:35 ` [RFC PATCH 22/39] sysctl: refresh namespace IPC " wen.yang
2026-10-28 16:35 ` [RFC PATCH 23/39] yama: preserve scope locking with numeric sysctl bounds wen.yang
2026-10-28 16:35 ` [RFC PATCH 24/39] net: refresh core sysctl limits without bound pointers wen.yang
2026-10-28 16:35 ` [RFC PATCH 25/39] sctp: resolve numeric ranges from the current namespace wen.yang
2026-10-28 16:35 ` [RFC PATCH 26/39] rds: preserve live reconnect limits with numeric bounds wen.yang
2026-10-28 16:35 ` [RFC PATCH 27/39] ipv4: preserve live and vector bounds with numeric sysctls wen.yang
2026-10-28 16:35 ` [RFC PATCH 28/39] net: refresh fragment and remaining dynamic sysctl bounds wen.yang
2026-10-28 16:35 ` [RFC PATCH 29/39] neighbour: derive sysctl ownership independently of bounds wen.yang
2026-10-28 16:35 ` [RFC PATCH 30/39] net: resolve sysctl owners from their backing data wen.yang
2026-10-28 16:35 ` [RFC PATCH 31/39] ipvs: derive sysctl ownership from numeric storage wen.yang
2026-10-28 16:35 ` [RFC PATCH 32/39] sysctl: remove obsolete nr_open bound variables wen.yang
2026-10-28 16:35 ` [RFC PATCH 33/39] ipv6: move address sysctl context to priv wen.yang
2026-10-28 16:35 ` [RFC PATCH 34/39] ipv4: move device " wen.yang
2026-10-28 16:35 ` [RFC PATCH 35/39] netfilter: move logger " wen.yang
2026-10-28 16:35 ` [RFC PATCH 36/39] parport: move device " wen.yang
2026-10-28 16:35 ` [RFC PATCH 37/39] net: move namespace " wen.yang
2026-10-28 16:35 ` [RFC PATCH 38/39] arm64: move vector-length " wen.yang
2026-10-28 16:35 ` [RFC PATCH 39/39] sysctl: remove the extra pointers and the compatibility bounds read wen.yang
2026-10-29 10:58 ` [RFC PATCH 00/39] sysctl: replace overloaded extra pointers with typed bounds and context David Laight
2026-11-02 15:40   ` Wen Yang
2026-11-02 16:54     ` David Laight
2026-11-04 18:32       ` Wen Yang [this message]

Reply instructions:

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

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

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

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

  git send-email \
    --in-reply-to=01b45745-38e9-48b3-9ac5-b62e6d57db2b@linux.dev \
    --to=wen.yang@linux.dev \
    --cc=david.laight.linux@gmail.com \
    --cc=joel.granados@kernel.org \
    --cc=linux-fsdevel@vger.kernel.org \
    --cc=linux-kernel@vger.kernel.org \
    /path/to/YOUR_REPLY

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

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox

all inboxes | Powered by JetHome®