NetBSD-Bugs archive

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index][Old Index]

kern/60035: CTLFLAG_PRIVATE can be bypassed for sysctl nodes with custom handlers in sysctl_dispatch()



>Number:         60035
>Category:       kern
>Synopsis:       CTLFLAG_PRIVATE can be bypassed for sysctl nodes with custom handlers in sysctl_dispatch()
>Confidential:   no
>Severity:       serious
>Priority:       medium
>Responsible:    kern-bug-people
>State:          open
>Class:          sw-bug
>Submitter-Id:   net
>Arrival-Date:   Wed Feb 25 01:55:00 +0000 2026
>Originator:     Matthias Petermann
>Release:        10.1_STABLE
>Organization:
>Environment:
NetBSD vhost.lan 10.1_STABLE NetBSD 10.1_STABLE (GENERIC) #0: Tue Feb 24 23:08:59 CET 2026  mpeterma%builder.lan@localhost:/u/sysbuild/obj/sys/arch/amd64/compile/GENERIC amd64

>Description:
Overview / motivation:
----------------------

I am trying to harden kernel message buffer access. In loginit(), I marked both kern.msgbufsize and kern.msgbuf as private (CTLFLAG_PERMANENT | CTLFLAG_PRIVATE).

In addition, in a custom jail secmodel, I deny KAUTH_REQ_SYSTEM_SYSCTL_PRVT from jail context to block private sysctl reads (including kern.msgbuf).

Despite that, dmesg still succeeds in scenarios where access should be denied.

Observed behavior:
------------------

dmesg reads the message buffer via sysctl(CTL_KERN, KERN_MSGBUF), and the sysctl calls return success even when private-node access should be blocked by policy.

Expected behavior:
------------------

Any read of a CTLFLAG_PRIVATE node should consistently require successful KAUTH_REQ_SYSTEM_SYSCTL_PRVT authorization, regardless of whether the node is served by sysctl_lookup() or by a custom sysctl_func handler.

>How-To-Repeat:
Root cause analysis:
--------------------

1. sysctl_dispatch() resolves the node via sysctl_locate().

2. If the resolved node has sysctl_func, dispatch calls that handler directly; it does not go through sysctl_lookup().

3. The strict final-node private authorization check is in sysctl_lookup() (rnode->sysctl_flags & CTLFLAG_PRIVATE => kauth_authorize_system(...SYSCTL_PRVT...)).

4. Therefore, for custom-handler nodes, final-node private enforcement can be skipped unless explicitly re-checked in dispatch.

5. kern.msgbuf/kern.msgbufsize are exactly such custom-handler nodes (sysctl_msgbuf), so they are affected by this gap.


>Fix:
In sysctl_dispatch(), after sysctl_locate() succeeds and before invoking a custom sysctl_func, perform a final target-node private authorization check equivalent to sysctl_lookup()?s check. This makes CTLFLAG_PRIVATE semantics consistent across both generic and custom-handler nodes.

---

diff --git a/sys/kern/kern_sysctl.c b/sys/kern/kern_sysctl.c
index 51a125541fbd423c33e8b32960ab152e09b7443a..7db8f660840f0b7160570a51f5ace971eefb516a 100644
--- a/sys/kern/kern_sysctl.c
+++ b/sys/kern/kern_sysctl.c
@@ -380,50 +380,59 @@ sysctl_relock(void)
 
 /*
  * ********************************************************************
  * the main sysctl dispatch routine.  scans the given tree and picks a
  * function to call based on what it finds.
  * ********************************************************************
  */
 int
 sysctl_dispatch(SYSCTLFN_ARGS)
 {
 	int error;
 	sysctlfn fn;
 	int ni;
 
 	KASSERT(rw_lock_held(&sysctl_treelock));
 
 	if (rnode && SYSCTL_VERS(rnode->sysctl_flags) != SYSCTL_VERSION) {
 		printf("sysctl_dispatch: rnode %p wrong version\n", rnode);
 		error = EINVAL;
 		goto out;
 	}
 
 	fn = NULL;
 	error = sysctl_locate(l, name, namelen, &rnode, &ni);
 
+	/*
+	 * Enforce private-node read permissions on the final target even when
+	 * dispatching through a custom sysctl handler rather than sysctl_lookup().
+	 */
+	if (error == 0 && l != NULL && (rnode->sysctl_flags & CTLFLAG_PRIVATE) &&
+	    (error = kauth_authorize_system(l->l_cred, KAUTH_SYSTEM_SYSCTL,
+	    KAUTH_REQ_SYSTEM_SYSCTL_PRVT, NULL, NULL, NULL)) != 0)
+		goto out;
+
 	if (rnode->sysctl_func != NULL) {
 		/*
 		 * the node we ended up at has a function, so call it.  it can
 		 * hand off to query or create if it wants to.
 		 */
 		fn = rnode->sysctl_func;
 	} else if (error == 0) {
 		/*
 		 * we found the node they were looking for, so do a lookup.
 		 */
 		fn = (sysctlfn)sysctl_lookup; /* XXX may write to rnode */
 	} else if (error == ENOENT && (ni + 1) == namelen && name[ni] < 0) {
 		/*
 		 * prospective parent node found, but the terminal node was
 		 * not.  generic operations associate with the parent.
 		 */
 		switch (name[ni]) {
 		case CTL_QUERY:
 			fn = sysctl_query;
 			break;
 		case CTL_CREATE:
 #if NKSYMS > 0
 		case CTL_CREATESYM:
 #endif /* NKSYMS > 0 */
 			if (newp == NULL) {



Home | Main Index | Thread Index | Old Index