pfr_fix_anchor unbounded slash-count loop causes size_t wraparound in bcopy/memset: kernel panic via DIOCRGETTABLES
| Field | Value |
|---|---|
| ID | DF-0362 |
| Status | new |
| Severity | High |
| CVSS 3.1 | CVSS:3.1/AV:L/AC:L/PR:H/UI:N/S:C/C:H/I:H/A:H |
| CWE | CWE-787 Out-of-bounds Write |
| File | sys/net/pf/pf_table.c |
| Lines | 1740-1755 |
| Area | net (pf firewall) |
| Confidence | certain |
| Discovered | 2026-07-01 |
| Reported | pending |
Summary
pfr_fix_anchor() strips leading / characters from a user-supplied
anchor string (pfrt_anchor, char[MAXPATHLEN]) using a counting loop
with no bound against the buffer size. Because pfrt_name immediately
follows pfrt_anchor in struct pfr_table, an attacker who fills the
entire anchor with / and sets pfrt_name[0]='/' makes the loop walk
one byte past the array boundary, causing off to exceed siz
(MAXPATHLEN=1024). The subsequent bcopy(path, anchor, siz - off)
computes siz - off as a size_t, wrapping to ~2^64, producing a
massive out-of-bounds read and write that immediately panics the kernel.
Root cause
pfr_fix_anchor (sys/net/pf/pf_table.c:1740-1755):
int
pfr_fix_anchor(char *anchor)
{
size_t siz = MAXPATHLEN; /* 1024 */
int i;
if (anchor[0] == '/') {
char *path;
int off;
path = anchor;
off = 1;
while (*++path == '/') /* line 1751: NO bound vs siz */
off++;
bcopy(path, anchor, siz - off); /* line 1753: wraps if off > siz */
memset(anchor + siz - off, 0, off); /* line 1754: wild pointer */
}
...
}
The struct layout (sys/net/pf/pfvar.h:1036-1041):
struct pfr_table {
char pfrt_anchor[MAXPATHLEN]; /* offset 0, 1024 bytes */
char pfrt_name[PF_TABLE_NAME_SIZE]; /* offset 1024, 32 bytes โ ADJACENT */
u_int32_t pfrt_flags;
u_int8_t pfrt_fback;
};
When the attacker fills all 1024 bytes of pfrt_anchor with / and sets
pfrt_name[0] = '/':
- The
while (*++path == '/')loop readsanchor[1]throughanchor[1023](all/), then readsanchor[1024]which ispfrt_name[0]=/โ one byte past the array boundary. offbecomes 1025, exceedingsiz(1024).siz - off:sizissize_t(unsigned 64-bit),offisint(1025). Theintis promoted tosize_t, then1024 - 1025wraps to0xFFFFFFFFFFFFFFFF(~2^64).bcopy(path, anchor, 0xFFFFFFFFFFFFFFFF)attempts to copy ~2^64 bytes โ an immediate out-of-bounds read frompathand write toanchorthat page-faults into a kernel panic.
Threat model & preconditions
- Attacker position: local user with access to
/dev/pf(typically root; commonly exposed to jails with devfs rules). - Privileges gained or impact: kernel panic (guaranteed DoS). Potential memory corruption for code execution depending on adjacent kernel memory layout.
- Required config or capabilities: open
/dev/pfdescriptor./dev/pfis mode 0600 root:wheel by default, but is frequently exposed to jails for firewall management. - Reachability:
DIOCRGETTABLESioctl callspfr_get_tables()(pf_table.c:1280) which callspfr_fix_anchor(filter->pfrt_anchor)directly with no prior anchor validation.DIOCRGETTABLESis: - Allowed without FWRITE (
pf_ioctl.c:1063โ only FREAD needed). - Allowed at securelevel > 1 (
pf_ioctl.c:1013โ in the break list). - Also reachable through
pfr_validate_table(pf_table.c:1728) viaDIOCRADDADDRS,DIOCRDELADDRS,DIOCRSETADDRS, etc. (the attacker setspfrt_name = {'/', 0, ...}which passes all name validation checks at lines 1719-1727 beforepfr_fix_anchorruns).
Proof of concept
PoC source: findings/poc/DF-0362/poc.c
Build & run
cc -o poc findings/poc/DF-0362/poc.c ./poc # requires read access to /dev/pf
Expected output
Fatal trap 12: page fault while in kernel mode cpuid = 0 KDB: stack backtrace: ... pfr_fix_anchor() pfr_get_tables() pfioctl() devioctl() ...
Impact
- Guaranteed kernel panic from any context that can issue
DIOCRGETTABLESโ a read-only operation on/dev/pf. - Defeats
securelevel > 1(the ioctl is explicitly in the allowed list atpf_ioctl.c:1013). - In jail configurations where
/dev/pfis exposed, a jailed root can panic the host kernel, breaking jail isolation. - The
size_twraparound meansbcopycorrupts kernel memory before faulting. With heap/stack grooming, this could be an arbitrary write primitive โ though the ~2^64 size makes the copy fault quickly, limiting the overwrite window to a small region.
Recommended fix
Bound the slash-counting loop against siz and validate NUL-termination
before any rewriting:
--- a/sys/net/pf/pf_table.c
+++ b/sys/net/pf/pf_table.c
@@ -1742,6 +1742,8 @@ pfr_fix_anchor(char *anchor)
size_t siz = MAXPATHLEN;
int i;
+ if (anchor[siz - 1] != '\0')
+ return (-1);
if (anchor[0] == '/') {
char *path;
int off;
@@ -1749,7 +1751,7 @@ pfr_fix_anchor(char *anchor)
path = anchor;
off = 1;
- while (*++path == '/')
+ while (off < siz && *++path == '/')
off++;
+ if (off >= siz)
+ return (-1);
bcopy(path, anchor, siz - off);
memset(anchor + siz - off, 0, off);
}
The NUL-termination check at the top ensures the loop can never read past
the array. The off < siz guard in the loop condition is defense-in-depth.
References
- OpenBSD fixed a similar
pfr_fix_anchorissue (anchor not NUL-terminated) in 2014. DIOCRGETTABLESis defined insys/net/pf/pfvar.h.
Timeline
- 2026-07-01 Discovered during automated audit.
- 2026-07-01 Reported to DragonFlyBSD security contact (pending).