โฌข DragonFlyBSD Kernel Audit
โ† dashboard
DF-0362

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] = '/':

  1. The while (*++path == '/') loop reads anchor[1] through anchor[1023] (all /), then reads anchor[1024] which is pfrt_name[0] = / โ€” one byte past the array boundary.
  2. off becomes 1025, exceeding siz (1024).
  3. siz - off: siz is size_t (unsigned 64-bit), off is int (1025). The int is promoted to size_t, then 1024 - 1025 wraps to 0xFFFFFFFFFFFFFFFF (~2^64).
  4. bcopy(path, anchor, 0xFFFFFFFFFFFFFFFF) attempts to copy ~2^64 bytes โ€” an immediate out-of-bounds read from path and write to anchor that 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/pf descriptor. /dev/pf is mode 0600 root:wheel by default, but is frequently exposed to jails for firewall management.
  • Reachability: DIOCRGETTABLES ioctl calls pfr_get_tables() (pf_table.c:1280) which calls pfr_fix_anchor(filter->pfrt_anchor) directly with no prior anchor validation. DIOCRGETTABLES is:
  • 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) via DIOCRADDADDRS, DIOCRDELADDRS, DIOCRSETADDRS, etc. (the attacker sets pfrt_name = {'/', 0, ...} which passes all name validation checks at lines 1719-1727 before pfr_fix_anchor runs).

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 at pf_ioctl.c:1013).
  • In jail configurations where /dev/pf is exposed, a jailed root can panic the host kernel, breaking jail isolation.
  • The size_t wraparound means bcopy corrupts 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.

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_anchor issue (anchor not NUL-terminated) in 2014.
  • DIOCRGETTABLES is defined in sys/net/pf/pfvar.h.

Timeline

  • 2026-07-01 Discovered during automated audit.
  • 2026-07-01 Reported to DragonFlyBSD security contact (pending).