Bug 296521 - bin/pfctl: pf's netlink conversion blocks non-VNET jails from using pfctl (no allow.pf / RTNL_F_ALLOW_NONVNET_JAIL equivalent)
Summary: bin/pfctl: pf's netlink conversion blocks non-VNET jails from using pfctl (no...
Status: New
Alias: None
Product: Base System
Classification: Unclassified
Component: kern (show other bugs)
Version: Unspecified
Hardware: Any Any
: --- Affects Some People
Assignee: freebsd-pf (Nobody)
URL:
Keywords:
Depends on:
Blocks:
 
Reported: 2026-07-04 18:24 UTC by László Károlyi
Modified: 2026-07-05 10:12 UTC (History)
3 users (show)

See Also:


Attachments

Note You need to log in before you can comment on or make changes to this bug.
Description László Károlyi 2026-07-04 18:24:49 UTC
I've upgraded from 14.4 to 15.1 and hit this. After a couple hours of investigating an interacting on the #freebsd IRC channel, I was asked to make a summary of what I've bumped into. This is the condensed summary of it.

Environment:
Introduced in 15.0-RELEASE, unchanged through 15.1-RELEASE and head (2026-07).
sys/netpfil/pf/pf_nl.c does not exist in 14.4; it exists in 15.0 already with
the restriction described below, so this is a 15.0 regression, not 15.1.

(Distinct from the securelevel/EPERM issue some hit after upgrading jails to
15.1 -- commit 47c12f20bf58/D56390, added between 15.0 and 15.1. That gates
on securelevel and affects VNET jails too. This report is about a separate,
unconditional gate that blocks non-VNET jails regardless of securelevel.)

Description:

15.0 converted pfctl/libpfctl from ioctl(/dev/pf) to netlink(4) (generic
netlink family "pf", sys/netpfil/pf/pf_nl.c). Every command in pf_cmds[]
sets `.cmd_priv = PRIV_NETINET_PF`, enforced unconditionally in
genl_handle_message() (sys/netlink/netlink_generic.c):

    if (cmd->cmd_priv != 0 && !nlp_has_priv(nlp, cmd->cmd_priv))
        return (EPERM);

prison_priv_check() (sys/kern/kern_jail.c) only grants PRIV_NETINET_PF to
jails with their own vnet (`pr_flags & PR_VNET`); there is no jail
permission that grants it to a non-VNET jail. Result: pfctl inside any
non-VNET jail now fails (EPERM) on essentially all operations -- rule
load, start/stop, tables, kill-states -- even with /dev/pf exposed via
devfs_ruleset, securelevel 0, and jailed root.

The old ioctl(/dev/pf) path (pf_ioctl.c) never called priv_check() at all;
access was gated purely by opening /dev/pf. That's why this worked
through 14.x for non-VNET jails, and why it's a regression rather than a
pre-existing restriction.

This is the same VNET-only restriction that routing sockets hit for
PRIV_NET_ROUTE -- and which *was* given a fix: commit 3a53fe2cc4b7
("jail: add allow.routing jail permission", D49843) added a
PR_ALLOW_ROUTING permission plus an RTNL_F_ALLOW_NONVNET_JAIL command
flag, checked in rtnl_handle_message() (sys/netlink/netlink_route.c), so
non-VNET jails can be opted in per command. pf's genl_cmd struct has no
equivalent flag, and no PR_ALLOW_PF exists.

How-To-Repeat:

1. Non-VNET jail, /dev/pf exposed via devfs_ruleset.
2. Inside jail: pfctl -e   (or pfctl -f pf.conf)
3. Get: pfctl: DIOCSTART (or DIOCADDRULE, etc.): Operation not permitted.
4. Same command from host, or from a VNET jail: succeeds.

Suggested Fix (modeled on D49843):

- Add PR_ALLOW_PF jail permission (sys/sys/jail.h, jail.conf(5)).
- Add a command flag to struct genl_cmd (sys/netlink/netlink_ctl.h),
  checked in genl_handle_message() alongside cmd_priv/cmd_securelevel, to
  let PR_ALLOW_PF satisfy cmd_priv for non-VNET jails.
- Tag the relevant pf_cmds[] entries in pf_nl.c with it -- likely
  everything currently gated by PRIV_NETINET_PF, restoring what non-VNET
  jails could already do via the old ioctl path pre-15.0.
- Document clearly: since non-VNET jails share the host's vnet0, this
  permission lets the jail rewrite the *host's* actual pf ruleset/state/
  tables, not an isolated copy -- same caveat as allow.routing, worth
  flagging given pf is often used for jail-facing security tooling
  (e.g. fail2ban-style jails reacting to hostile input).

Workarounds in the meantime:
- Convert the jail to VNET (isolated pf instance, no gap, but requires
  epair/bridge rearchitecting).
- Manage the jail's pf rules from the host instead (anchor + `pfctl -a
  jailname`, or a host-side proxy acting on the jail's behalf).
- Keeping an old (14.4) pfctl binary in the jail to keep using the ioctl
  path works but is unsupported/fragile and re-opens the same privilege
  gap for jails handling untrusted input -- not recommended.
Comment 1 Marek Zarychta 2026-07-05 08:30:27 UTC
Is this an actual bug, or simply a stricter implementation?

For non-VNET jails, PF is expected to be managed from the host. As far as I understand, that's the recommended and documented deployment model.

On the other hand, if this used to work, perhaps it should still be allowed for compatibility reasons.

What is the intended behaviour for the other firewalls? In particular, are IPFW and IPF expected to be manageable from non-VNET jails, or are they managed exclusively from the host?
Comment 2 Marek Zarychta 2026-07-05 10:12:10 UTC
Perhaps this behaviour should be discussed on the freebsd-net@ mailing list.

Currently, non-VNET jails can be permitted to manipulate routing tables via the security.jail.param.allow.routing setting. If modifying the FIB is considered acceptable in a non-VNET jail under controlled circumstances, then perhaps firewall management should be governed by a similar dedicated permission or sysctl knob, rather than being treated as an all-or-nothing capability.

At the very least, it would be useful to clarify whether the current behaviour is an intentional policy decision or an unintended regression.