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

Lifecycle bugs in edge handler cleanup: UAF on replaced edge_sock, missing thread join, soclose+sofree imbalance

Summary

LATENT (dead code). Multiple: (a) edge_handler caches so=ctx.edge_sock :195 but cleanup :227-228 uses global if edge_conf replaced it between -> frees wrong socket leaks original. (b) edge_stop soclose :355 + handler sofree :228 = 2 refcount drops vs 1 soreference :259 -> socket freed while in use. (c) MOD_UNLOAD :476-490 sets running=0 closes sockets but NEVER joins edge_td -> handler thread runs after module code unmapped. (d) MOD_UNLOAD never clears ipfw_sync_send_state_prt -> dangling fn ptr. Fix: use cached so+thread join+NULL ptr.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-0706 Β· 10 files
FileTypeDescriptionSize
ipfw3_sync_lifecycle_probe.c trigger-source issues every IP_FW_SYNC_* opcode via IP_FW_X; proves all are silently no-op'd by the dispatcher 4.6 KB view raw
build.sh build-script cc -O2 -Wall 187 B view raw
run.sh run-script kldload ipfw3 + ipfw3_basic + run probe (does NOT kldunload β€” separate rn_flush panic) 912 B view raw
run.log run-log all 7 opcodes return rc=0; no panic 934 B view raw
env.txt environment uname, cc version, kldstat 284 B view raw
VERDICT.md verdict this analysis β€” LATENT dead code, same as DF-0704/0705 6.1 KB ↓ raw
README.md readme build/run/expected 1.1 KB ↓ raw
fix.diff suggested-fix defensive fix for (a) cached so cleanup, (b) edge_stop no soclose + join, (c) MOD_UNLOAD joins edge_td, (d) clears fn ptrs 4.8 KB view raw
../fix_build_combined.log build-log Combined 41-finding kernel build (rc=0, -Werror clean) 5.6 MB ↓ download
../fix_build_summary.txt build-summary Summary of the combined 41-finding kernel build 826 B view raw
README.md readme build/run/expected
↓ download raw

DF-0706 β€” PoC

Lifecycle bugs in ipfw3 sync edge handler cleanup (UAF on replaced edge_sock, missing thread join, soclose+sofree imbalance, dangling ipfw_sync_send_state_prt).

Build

./build.sh

Requires DragonFlyBSD cc (gcc 8.3 on master guest).

Run

ssh dfbsd         # root
cd poc/DF-0706
./run.sh

run.sh kldloads ipfw3.ko + ipfw3_basic.ko then fires every IP_FW_SYNC_* opcode via IP_FW_X. On the default kernel every opcode returns rc=0 errno=0 with no effect β€” the dispatcher falls through because ip_fw3_ctl_sync_ptr is never assigned. Guest stays up, no panic. This proves the cited lifecycle bugs cannot fire on a default kernel.

Do NOT kldunload ipfw3_basic after this test: there is a separate, unrelated panic in rn_flush() (radix-tree cleanup) on this master build that is NOT the DF-0706 bug. The serial log will show Stopped at rn_flush+0x1d. (Reset the guest with vm.sh reset if you accidentally trigger it.)

Expected

All IP_FW_SYNC_* opcodes return success with no observable effect; guest stays up. See VERDICT.md for the full dead-code analysis and fix.diff for the defensive fix.

VERDICT.md verdict this analysis β€” LATENT dead code, same as DF-0704/0705
↓ download raw

DF-0706 β€” Lifecycle bugs in edge handler cleanup: UAF on replaced edge_sock, missing thread join, soclose+sofree imbalance

Verdict: NOT REPRODUCED (LATENT β€” entire ip_fw3_sync.c is dead code on a default kernel)

The finding cites four real defects in sys/net/ipfw3_basic/ip_fw3_sync.c:

# Bug Mechanism
(a) UAF on replaced edge_sock edge_socket_handler caches so at :195 for the receive loop but uses fw3_sync_ctx.edge_sock for cleanup at :227-228; if edge_conf replaced the global meanwhile, cleanup hits the wrong socket and leaks the original
(b) soclose+sofree imbalance edge_stop calls soclose (:355); handler calls sofree (:228); only one soreference was added at edge_start :259 β†’ two ref-drops for one ref-add
(c) MOD_UNLOAD never joins edge_td ip_fw3_sync_modevent at :480-484 sets running=0, calls soclose, and edge_td = NULL β€” but the handler thread keeps running until its receive returns; if the module is unmapped first, the handler executes unmapped code
(d) MOD_UNLOAD never clears ipfw_sync_send_state_prt after unload the global still points at ip_fw3_sync_send_state which is now unmapped memory

All four are real defects in code that can never execute on a default kernel. The entire ip_fw3_sync.c file is dead code, for two independent reasons that each suffice on their own:

Why the code paths are unreachable

(1) ip_fw3_ctl_sync_ptr is never assigned. The IP_FW_SYNC_ sockopt opcodes are dispatched in sys/net/ipfw3/ip_fw3.c at lines 1113-1128 only inside if (ip_fw3_ctl_sync_ptr != NULL). That pointer is initialised to NULL (line 133) and never assigned anywhere in the tree*:

$ grep -rn 'ip_fw3_ctl_sync_ptr =' sys/
sys/net/ipfw3/ip_fw3.c:133:ip_fw_ctl_t  *ip_fw3_ctl_sync_ptr = NULL;

So every IP_FW_SYNC_* opcode silently falls through with error = 0 (setsockopt returns success with no effect). Confirmed in run.log.

(2) ip_fw3_sync_modevent is never called. The companion function pointer ipfw_sync_send_state_prt (and ipfw_sync_install_state_prt) would be set by ip_fw3_sync_modevent(MOD_LOAD) (ip_fw3_sync.c:518-520). But ip_fw3_sync_modevent is never invoked by anyone β€” the ipfw3_basic module hook (ipfw3_basic_modevent, ip_fw3_basic.c:657-658) calls only ip_fw3_state_modevent and ip_fw3_table_modevent, skipping sync entirely:

$ grep -rn 'ip_fw3_sync_modevent\|ip_fw3_state_modevent\|ip_fw3_table_modevent' sys/
sys/net/ipfw3_basic/ip_fw3_basic.c:657:    ip_fw3_state_modevent(type);
sys/net/ipfw3_basic/ip_fw3_basic.c:658:    ip_fw3_table_modevent(type);
sys/net/ipfw3_basic/ip_fw3_sync.h:86: void ip_fw3_sync_modevent(int type);
sys/net/ipfw3_basic/ip_fw3_sync.c:514: ip_fw3_sync_modevent(int type)
sys/net/ipfw3_basic/ip_fw3_state.c:712: ip_fw3_state_modevent(int type)
sys/net/ipfw3_basic/ip_fw3_table.c:616: ip_fw3_table_modevent(int type)

state and table are called from ipfw3_basic_modevent; sync is declared and defined but not called from there. So MOD_LOAD of ipfw3_basic never sets ipfw_sync_send_state_prt, and MOD_UNLOAD never runs the cleanup code at all.

Empirical confirmation

run.log shows every IP_FW_SYNC_* opcode returning rc=0 errno=0 (silent fall-through) after kldload ipfw3.ko and kldload ipfw3_basic.ko on the default with-src kernel (#0 build, INVARIANTS ON):

[*] loading ipfw3.ko + ipfw3_basic.ko
[*] running as: uid=0 euid=0
[fire IP_FW_SYNC_EDGE_CONF       opcode=84 plen=8] rc=0 errno=0
[fire IP_FW_SYNC_EDGE_START      opcode=85 plen=4] rc=0 errno=0
[fire IP_FW_SYNC_EDGE_STOP       opcode=86 plen=4] rc=0 errno=0
[fire IP_FW_SYNC_CENTRE_CONF     opcode=89 plen=4] rc=0 errno=0
[fire IP_FW_SYNC_CENTRE_START    opcode=90 plen=4] rc=0 errno=0
[fire IP_FW_SYNC_SHOW_STATUS     opcode=83 plen=4] rc=0 errno=0
[fire IP_FW_SYNC_SHOW_CONF       opcode=82 plen=64] rc=0 errno=0
[+] All opcodes returned without panic.

If the buggy paths were live, EDGE_CONF would socreate() an edge_sock, EDGE_START would soreference() + kthread_create the handler thread, and EDGE_STOP would exercise the soclose+sofree imbalance. None of that happens β€” the dispatcher returns success without invoking any sync code.

MOD_UNLOAD of ipfw3_basic completes without invoking ip_fw3_sync_modevent (proven by grep), so neither (c) nor (d) can fire β€” the conditions they require (edge_td exists / ipfw_sync_send_state_prt is set) never arise.

Conclusion

Consistent with DF-0704 and DF-0705 (which both analysed other defects in this same file and reached the same dead-code conclusion): the cited bugs are real defects in the source code but cannot be triggered on a default kernel because the entire ipfw3_sync subsystem is unwired. Marked not_reproduced β€” impact=none, latent.

Suggested fix

fix.diff is a defensive, in-place fix for all four cited bugs. Because the code is currently dead, the fix cannot be runtime-tested on the default kernel (no trigger path exists); instead the diff is validated for git apply --check and compiles cleanly. The day someone wires ip_fw3_sync_modevent(type) into ipfw3_basic_modevent (a one-line addition) and assigns ip_fw3_ctl_sync_ptr, the fixes are already in place:

  • (a) edge_socket_handler now uses the cached so for cleanup too
  • (b) edge_stop no longer calls soclose; it signals the handler via soshutdown and kthread_joins edge_td, leaving the single ref-drop to the handler's sofree
  • (c) ip_fw3_sync_modevent(MOD_UNLOAD) now soshutdowns the socket, kthread_joins edge_td, and only then frees resources
  • (d) MOD_UNLOAD clears ipfw_sync_send_state_prt = NULL and ipfw_sync_install_state_prt = NULL first, before any other teardown

The fix also picks up DF-0704's per-type m_len validation in the handler as a bonus (it touches the same function). That hardens the receive loop against short datagrams if the code is ever enabled.

The fix is defensive only β€” it does not enable the dead code (that is upstream's call) but ensures the bugs are closed if/when the code is hooked up.

Fix verification

not_testable

n/a

see evidence pack

Confirmed kernel references

β€”

Detail

Exploit chain

none

Evidence (decisive lines)

β€”

Verdict

LATENT dead code. ipfw3_sync edge handler lifecycle bugs. ip_fw3_ctl_sync_ptr never assigned. Same as DF-0704/0705.