β¬’ DragonFlyBSD Kernel Audit
← triage Β· dashboard
DF-3367

RTM_CHANGE with gateway==destination permanently leaks the rtentry (key chunk + ifa ref) and desynchronizes per-CPU routing tables; subsequent re-ADD of the same destination panics the kernel

Summary

route_output_change_callback calls rt_setgate(rt, rt_key(rt), gw); for a host-gateway route whose new gateway equals its own destination, rt_setgate's EADDRNOTAVAIL path deletes the route from the current CPU's tree via nested RTM_DELETE and returns an error. rtsearch_msghandler's temporary ref is then dropped to zero without a finalizing rtfree: the rtentry, key chunk and ifa reference are orphaned forever (out of tree, refcnt 0, no holders). Because the search replies on the first CPU error, CPUs 1..N never see the change and keep the route - per-CPU table divergence; a later RTM_ADD of the same destination succeeds on CPU0 and hits EEXIST on CPU1 -> deterministic panic at route.c:763. VERIFIED with plain route(8): 200 add/change cycles -> routetbl 1.48K/257K -> 3.82K/669K (+412KB permanent leak); routes invisible on cpu0 while 'route delete' succeeds via the surviving CPU1..N copies (divergence proven); panic leg traced but deliberately not triggered. Privileged trigger only (RTM_CHANGE gated): permanent memory exhaustion + routing-plane divergence + one-command delayed panic. Fix: RTFREE-style finalization of the temp ref (or make rt_setgate's failure path non-mutating atomically on all CPUs).

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3367 Β· 5 files
FileTypeDescriptionSize
run.sh β€” 227 B view raw
leakcheck.log β€” 18 B view raw
leakcheck2.log β€” 287 B view raw
README.md β€” 2.7 KB ↓ raw
verdict.json β€” 3.1 KB view raw

DF-3367 β€” RTM_CHANGE with gateway == destination permanently leaks the rtentry and desynchronizes the per-CPU routing tables

Root cause (all in sys/net/route.c)

route_output_change_callback (rtsock.c:908-972) calls rt_setgate(rt, rt_key(rt), gateway) for RTM_CHANGE. For a host-gateway route whose new gateway equals its own destination, rt_setgate hits the EADDRNOTAVAIL path (route.c:1193-1205): it deletes the route from the current CPU's tree via a nested rtrequest(RTM_DELETE, ...) and returns an error.

Refcount trace at that moment (rtsearch_msghandler route.c:1726-1729):

  • callback entry: rt->rt_refcnt++ β†’ 1
  • nested RTM_DELETE (route.c:824 ++rt->rt_refcnt β†’ 2; route.c:866 rtfree β†’ 1; route removed from tree, RTF_UP cleared, rt_key/rt_ifa intact)
  • callback returns error β†’ rt->rt_refcnt-- β†’ 0

Nobody holds a pointer anymore: the rtentry, its key chunk, and its ifa reference are never released β€” a permanent leak on that CPU. Because rtsearch_msghandler replies (and stops) on the first CPU error (route.c:1751), CPUs 1..N-1 never run the change and keep the route: the per-CPU tables diverge. A later RTM_ADD of the same destination then succeeds on CPU0 and fails EEXIST on CPU1 β†’ guaranteed panic("rtrequest1_msghandler: rtrequest table req ...") (route.c:763-768). (The panic leg was traced but deliberately not triggered during verification; the leak and divergence legs were verified empirically.)

Threat

Trigger requires routing-socket write privilege (root), so this is a robustness/lifetime bug, not an escalation: a privileged (or compromised routing daemon, e.g. via route(8)) process can permanently consume kernel memory and panic the system.

Verified on the guest (leakcheck.log / leakcheck2.log)

BEFORE
           routetbl   1.48K    257K       0    390M    3.16K
AFTER-200-CYCLES
           routetbl   3.82K    669K       0    390M    5.89K
routes visible on cpu0: 0
delete host 10.99.2.1
rc=0            <-- succeeded although cpu0 has no such route:
                    cpus 1..5 still do (divergence proven)

200 add+change cycles (+2340 in-use routetbl allocations, +412 KB); every route is invisible on cpu0 yet still present on the other CPUs; cpu0's copies are out-of-tree with refcount 0 β€” unfreeable.

Fix sketch

In rtsearch_msghandler, drop the temporary reference via rtfree()-safe logic (e.g. RTFREE(rt) after the callback β€” route.h:446 decrements and finalizes at 0), or make rt_setgate's EADDRNOTAVAIL path not mutate the tree and instead return the error to the outer RTM_CHANGE, which should then fail cleanly on every CPU. Also revisit rtredirect_oncpu (route.c:425, DF-0399) which reaches the same nested-delete with the return value ignored.

Fix verification

not_testable
per-fix-DF-3367

Confirmed kernel references

Detail

Exploit chain

root (or compromised routing daemon): route add -host A GW; route change -host A A -> leaked rtentry+ifa on cpu0, table divergence; re-adding A later panics the kernel (rtrequest1_msghandler route.c:763-768). No unprivileged trigger exists (route_output requires privilege for RTM_CHANGE, rtsock.c:738-740).

Evidence (decisive lines)

["findings/poc/DF-3367/leakcheck2.log β€” vmstat -m routetbl BEFORE 1.48K/257K -> AFTER-200-CYCLES 3.82K/669K; 'routes visible on cpu0: 0'; `route delete -host 10.99.2.1` returns rc=0 (route exists only on cpus 1..N = divergence proven)", 'findings/poc/DF-3367/leakcheck.log β€” first 100-cycle confirmation', 'findings/poc/DF-3367/README.md β€” full refcount trace route.c:1726-1729 + 1193-1205']

PoC changes

no C PoC needed β€” plain route(8) add/change cycles reproduce; measurement via vmstat -m (M_RTABLE = 'routetbl')

Verified recommended fix

Make rtsearch_msghandler drop its temp ref with RTFREE()-style finalization (route.c:1729), or stop rt_setgate's EADDRNOTAVAIL path from mutating the tree and fail the outer RTM_CHANGE atomically on all CPUs.

Verdict

RTM_CHANGE whose new gateway equals the route's own destination takes rt_setgate's EADDRNOTAVAIL path (route.c:1193-1205), which deletes the route from the current CPU's tree, while rtsearch_msghandler's temporary reference (route.c:1726-1729) is dropped to zero without a finalizing rtfree: the rtentry, its key chunk and its ifa reference leak permanently on that CPU, and the per-CPU routing tables diverge (CPUs 1..N keep the route). Verified empirically as root with plain route(8): 200 add+change cycles grew the routetbl malloc type from 1.48K/257K to 3.82K/669K in-use allocations with zero routes visible on cpu0, and route delete -host 10.99.2.1 (a route cpu0 does not have) succeeded via the surviving CPU1..N copies. A subsequent RTM_ADD of the same destination deterministically panics via route.c:763-768 (cpu0 adds, cpu1 EEXIST) β€” traced, deliberately not triggered during verification. Privileged trigger only (routing socket write), hence Low severity: robustness/lifetime bug, memory-exhaustion and panic DoS by root or a compromised routing daemon.