PR 291981: authpf under netlink, and a pfctl adoption flag

Avinash Duduskar <[email protected]> Fri, 24 Jul 2026 09:02:41 +0530
Newsgroups gmane.os.freebsd.devel.pf4freebsd
Message-ID <[email protected]>
From: Avinash H. Duduskar <[email protected]>

Hi all,

Kristof asked me to bring this here from a private thread; it is a
design question about authpf under the pf netlink conversion, and a
small fix.

authpf breaks on 15.x (PR 291981) at the exec.  change_filter hands the
child the root /dev/pf fd as -p /dev/fd/N, so its ioctls still work; but
that flag carries only the device fd, and pfctl's first privileged
netlink command (PFNL_CMD_BEGIN_ADDRS, since ba2a9207862) rides a fresh
socket it opens after the setresuid drop, so the kernel's per-command
check sees the dropped cred and EPERMs.  Nobody authenticates.  The
DIOCBEGINADDRS in the report is a stale label; the call is netlink now.

The fix is the netlink analogue of -p /dev/fd.  authpf already holds a
root netlink socket in its pfctl_handle; the patch lets the exec'd
pfctl adopt it instead of opening its own.

Why a split rather than just keeping the handle: authpf does keep it.
pfctl_open runs at authpf.c:137 as root, before the drop.  But the
handle is authpf's memory; what crosses the exec is the two fds inside
it.  The device fd has an intake on the other side, -p /dev/fd/N.  The
netlink fd is inherited too, but nothing takes it: the exec'd pfctl's
own pfctl_open()/snl_init() unconditionally opens a fresh socket, which
now carries the dropped cred.  The split gives the inherited fd an
entry point: snl_init_fd adopts an fd it is handed, snl_init keeps
opening its own, so no existing consumer changes.  And it stays small
enough to MFC, which a PR against a release needs.  Keeping everything
in-process instead would mean teaching libpfctl to load a ruleset file,
moving pfctl's parser: a much larger change, not MFC material, and
nothing here forecloses it.

 static inline bool
-snl_init(struct snl_state *ss, int netlink_family)
+snl_init_fd(struct snl_state *ss, int fd)
 {
 	memset(ss, 0, sizeof(*ss));

-	ss->fd = socket(AF_NETLINK, SOCK_RAW, netlink_family);
-	if (ss->fd == -1)
-		return (false);
+	ss->fd = fd;
 	ss->init_done = true;
 	... [EXT_ACK, SO_RCVBUF, buffers unchanged] ...
 	return (true);
 }
+
+static inline bool
+snl_init(struct snl_state *ss, int netlink_family)
+{
+	int fd = socket(AF_NETLINK, SOCK_RAW, netlink_family);
+	if (fd == -1) {
+		memset(ss, 0, sizeof(*ss));
+		ss->fd = -1;
+		return (false);
+	}
+	return (snl_init_fd(ss, fd));
+}

snl_init_fd owns the fd it adopts (snl_free closes it), distinct from
snl_clone, which shares an existing state's fd without taking ownership.

This leans on the 2024 libpfctl work more than it adds a mechanism.
044243fcc9b4 said it plainly: pfctl_open opens both /dev/pf and a
netlink socket.  It exposed the device half through pfctl_fd();
pfctl_nl_fd() now mirrors that for the socket, and the flag is how pfctl
takes that fd back.  The _h() series (324fd7ec404 introduced
pfctl_add_rule_h, taking a handle rather than the fd it didn't use)
carried the in-process consumers across the drop, ftp-proxy and
tftp-proxy included; authpf is the one they cannot reach, because it
execs for pfctl's parser, which is not a library.

Those proxies keep the handle in-process; across the exec authpf hands
the child its two fds instead: the device fd via -p and the socket via
the flag (a bare number, no fdescfs needed), matching pf's two control
paths, the device ioctl path and netlink.

Adopting the socket adopts exactly the authority the kernel already
checks per command, and pfctl is not setuid, so the flag grants nothing
on its own: point it at an unprivileged socket and it is denied.

One caveat: the socket is NETLINK_GENERIC, so its root cred reaches any
genl family, not just pf, though abusing that still means subverting a
pfctl already parsing root-owned config.

It does not compete with mac_bsdpriv.  That grants by identity; authpf
hands one session's authority to an arbitrary uid transiently, which a
per-session socket expresses and an ambient grant cannot without giving
that uid pf access outside authpf.  And it restores zero-config authpf,
which needs an answer whatever happens with the MAC work.

The snl details are open, and melifaro's more than mine: whether
snl_init_fd is documented or stays a bare primitive with adoption
surfaced in libpfctl, and how the flag looks.  I built the whole change
(about 80 lines across five files) and ran it on 15.1: stock reproduces
PR 291981, the patched build authenticates and passes traffic, and an
unprivileged pfctl is still denied.  Mostly I want to know: does this
read as idiomatic for authpf now that pf is on netlink?  If it does, I
will put the full diff up on Phabricator.

Best,
Avinash