git: a94a6326507c - main - pf tests: exercise pfctl -l across a privilege drop
- Go to: [ bottom of page ] [ top of archives ] [ this month ]
Date: Fri, 30 Oct 2026 14:08:12 UTC
The branch main has been updated by kp:
URL: https://cgit.FreeBSD.org/src/commit/?id=a94a6326507c5a901adee15bf3844eaf60479ab1
commit a94a6326507c5a901adee15bf3844eaf60479ab1
Author: Avinash Duduskar <avinash.duduskar@gmail.com>
AuthorDate: 2026-10-26 14:38:12 +0000
Commit: Kristof Provost <kp@FreeBSD.org>
CommitDate: 2026-10-30 14:07:25 +0000
pf tests: exercise pfctl -l across a privilege drop
Open /dev/pf and a generic Netlink socket as root, drop to nobody and
exec pfctl with both inherited, the way authpf(8) runs it. Without -l
the load is refused with EPERM; with it the rules land in the anchor.
An unusable descriptor is an error, not a fallback to a new socket.
Skips when fdescfs is not mounted, which -p /dev/fd/N needs.
PR: 291981
Signed-off-by: Avinash Duduskar <avinash.duduskar@gmail.com>
Reviewed by: kp
Pull Request: https://github.com/freebsd/freebsd-src/pull/2508
---
tests/sys/netpfil/pf/Makefile | 3 ++
tests/sys/netpfil/pf/authpf.sh | 75 +++++++++++++++++++++++++++++++++++++
tests/sys/netpfil/pf/pfctl_adopt.py | 43 +++++++++++++++++++++
3 files changed, 121 insertions(+)
diff --git a/tests/sys/netpfil/pf/Makefile b/tests/sys/netpfil/pf/Makefile
index c95bac83c92c..be3ac9f91486 100644
--- a/tests/sys/netpfil/pf/Makefile
+++ b/tests/sys/netpfil/pf/Makefile
@@ -5,6 +5,7 @@ TESTS_SUBDIRS+= ioctl
ATF_TESTS_SH+= altq \
anchor \
+ authpf \
counters \
debug \
divert-to \
@@ -89,6 +90,7 @@ ${PACKAGE}FILES+= \
frag-overreplace.py \
frag-overhole.py \
frag-adjhole.py \
+ pfctl_adopt.py \
pfsync_defer.py \
pft_ether.py \
pft_read_ipfix.py \
@@ -107,6 +109,7 @@ ${PACKAGE}FILESMODE_frag-overlimit.py= 0555
${PACKAGE}FILESMODE_frag-overreplace.py= 0555
${PACKAGE}FILESMODE_frag-overhole.py= 0555
${PACKAGE}FILESMODE_frag-adjhole.py= 0555
+${PACKAGE}FILESMODE_pfctl_adopt.py= 0555
${PACKAGE}FILESMODE_pfsync_defer.py= 0555
${PACKAGE}FILESMODE_pft_ether.py= 0555
${PACKAGE}FILESMODE_pft_read_ipfix.py= 0555
diff --git a/tests/sys/netpfil/pf/authpf.sh b/tests/sys/netpfil/pf/authpf.sh
new file mode 100644
index 000000000000..38cb80e94dc1
--- /dev/null
+++ b/tests/sys/netpfil/pf/authpf.sh
@@ -0,0 +1,75 @@
+#
+# SPDX-License-Identifier: BSD-2-Clause
+#
+# Copyright (c) 2026 Avinash Duduskar <avinash.duduskar@gmail.com>
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions
+# are met:
+# 1. Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# 2. Redistributions in binary form must reproduce the above copyright
+# notice, this list of conditions and the following disclaimer in the
+# documentation and/or other materials provided with the distribution.
+#
+# THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
+# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
+# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+# SUCH DAMAGE.
+
+. $(atf_get_srcdir)/utils.subr
+
+atf_test_case "pfctl_adopt" "cleanup"
+pfctl_adopt_head()
+{
+ atf_set descr 'pfctl -l across a privilege drop, as authpf(8) runs it'
+ atf_set require.user root
+ atf_set require.progs python3
+}
+
+pfctl_adopt_body()
+{
+ pft_init
+
+ # pfctl is handed /dev/pf as -p /dev/fd/N, which needs fdescfs.
+ if ! ( exec 3</dev/null; [ -e /dev/fd/3 ] ); then
+ atf_skip "This test requires fdescfs mounted on /dev/fd"
+ fi
+
+ helper=$(atf_get_srcdir)/pfctl_adopt.py
+
+ vnet_mkjail alcatraz
+ jexec alcatraz pfctl -e
+
+ # Without -l pfctl opens its own, unprivileged socket (PR 291981).
+ echo "pass" | atf_check -s not-exit:0 -e ignore \
+ jexec alcatraz ${helper} -a authpf_test -f -
+ atf_check -s exit:0 -o not-match:"authpf_test" \
+ jexec alcatraz pfctl -q -sA
+
+ # An unusable descriptor is an error, not a fallback to a new socket.
+ atf_check -s exit:1 -e ignore \
+ jexec alcatraz sh -c 'pfctl -l 3 -sr 3</dev/null'
+
+ echo "pass" | atf_check -s exit:0 \
+ jexec alcatraz ${helper} -l -a authpf_test -f -
+ atf_check -s exit:0 -o match:"^pass all" \
+ jexec alcatraz pfctl -q -a authpf_test -sr
+}
+
+pfctl_adopt_cleanup()
+{
+ pft_cleanup
+}
+
+atf_init_test_cases()
+{
+ atf_add_test_case "pfctl_adopt"
+}
diff --git a/tests/sys/netpfil/pf/pfctl_adopt.py b/tests/sys/netpfil/pf/pfctl_adopt.py
new file mode 100755
index 000000000000..480f6e58a1ed
--- /dev/null
+++ b/tests/sys/netpfil/pf/pfctl_adopt.py
@@ -0,0 +1,43 @@
+#!/usr/bin/env python3
+#
+# SPDX-License-Identifier: BSD-2-Clause
+#
+# Copyright (c) 2026 Avinash Duduskar <avinash.duduskar@gmail.com>
+#
+# Run pfctl as authpf(8) does: open /dev/pf and a Netlink socket as root,
+# drop to nobody, exec pfctl with -p /dev/fd/N and, given -l, -l M.
+
+import os
+import pwd
+import socket
+import sys
+
+AF_NETLINK = 38 # sys/socket.h; not exported by the socket module
+NETLINK_GENERIC = 16 # netlink/netlink.h
+
+
+def main():
+ args = sys.argv[1:]
+ adopt = False
+ if args and args[0] == "-l":
+ adopt = True
+ args = args[1:]
+
+ dev = os.open("/dev/pf", os.O_RDWR)
+ os.set_inheritable(dev, True)
+ nl = socket.socket(AF_NETLINK, socket.SOCK_RAW, NETLINK_GENERIC)
+ os.set_inheritable(nl.fileno(), True)
+
+ pw = pwd.getpwnam("nobody")
+ os.setgroups([pw.pw_gid])
+ os.setgid(pw.pw_gid)
+ os.setuid(pw.pw_uid)
+
+ argv = ["pfctl", "-p", "/dev/fd/%d" % dev]
+ if adopt:
+ argv += ["-l", str(nl.fileno())]
+ os.execv("/sbin/pfctl", argv + args)
+
+
+if __name__ == "__main__":
+ main()