Heap OOB write via byte-swapped alias_port used as array index: every NAT deployment corrupts heap ~1.6% of connections
Summary
pick_alias_port(:439): s->alias_port=htons(krandom()%ALIAS_RANGE+ALIAS_BEGIN) stores NETWORK byte order. Line :423 alias->tcp_in[s->alias_port-ALIAS_BEGIN]=s2 uses raw value as array index WITHOUT ntohs. On little-endian x86-64 only DragonFly target: host v=0x0401(1025) -> htons=0x0104(260) -> index 260-1024 wraps unsigned to ~64772 past tcp_in[64511]. ~1.6% of new connections when low byte of host value is 0-3. OOB write of 8-byte pointer past array end into adjacent heap. Read side(:204) uses same wrong index so connection appears to work while corrupting. Every NAT deployment. Remote-triggered by any host sending traffic through NAT. cfg_alias ~1.5MB single OOB slot wild pointer store into neighboring heap. Fix: store host-order index separately use ntohs consistently.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-0569 Β· 21 files| File | Type | Description | Size | |
|---|---|---|---|---|
| alias_port_oob_proof.c | trigger-source | Deterministic arithmetic proof: scans all 64511 host ports, shows 1008 (1.56%) produce OOB indices [-1020,-1] | 4.0 KB | view raw |
| nat_oob_trigger.c | trigger-source | UDP flow driver: drives N outbound UDP flows through NAT to exercise pick_alias_port | 2.2 KB | view raw |
| nat_oob_trigger_tcp.c | trigger-source | TCP connection driver (slow, reference only) | 2.7 KB | view raw |
| raw_syn_trigger.c | trigger-source | Raw SYN packet sender for fast TCP path triggering (hits separate checksum bug) | 3.8 KB | view raw |
| run.sh | build-script | Automated NAT setup + UDP flow driver | 2.4 KB | view raw |
| build.sh | build-script | Builds arithmetic proof and trigger | 379 B | view raw |
| analyze_oob.sh | build-script | Analyzes OOB entries in state table | 975 B | view raw |
| build.log | build-log | Build output for proof + trigger + patched module | 450 B | view raw |
| run.log | run-log | Unpatched run: 5000 UDP flows, 102 TCP entries (OOB confirmed), low-byte distribution 27/25/26/24/0 | 1.7 KB | view raw |
| fix_run.log | run-log | Patched run: 5000 UDP flows, 0 TCP entries (fix validated) | 708 B | view raw |
| run_state_table.txt | leak-sample | Full unpatched state table (15497 states, 102 TCP entries = OOB evidence) | 873.9 KB | β download |
| fix_run_state_table.txt | run-log | Patched state table summary: 0 TCP entries | 342 B | view raw |
| panic.txt | panic-signature | Kernel panic from raw SYN packets (separate in_delayed_cksum bug, not DF-0569) | 267 B | view raw |
| panic_cleanup.txt | panic-signature | Re-verification (2026-07-16): Fatal trap 12 in nat_cleanup_func_dispatch deref of garbage nat_state2 (0x1af) -> s2->timestamp(0x18) fault at 0x1c7. Cleanup callout crashed on heap corrupted by the DF-0569 OOB writes after heavy NAT traffic. | 675 B | view raw |
| env.txt | environment | Guest uname, cc version, interface config | 309 B | view raw |
| fix.diff | suggested-fix | 8-site ntohs() fix for all byte-swapped array index uses | 1.8 KB | view raw |
| fix_build.log | build-log | Patched module build log | 618 B | view raw |
| VERDICT.md | verdict | Full analysis: mechanism, evidence, exploit chain, fix validation | 8.3 KB | β raw |
| README.md | readme | Human-readable summary with reproduction instructions | 3.9 KB | β 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 |
DF-0569 β Heap OOB write via byte-swapped alias_port used as array index
Severity: High
Class: CWE-787 Out-of-bounds Write
File: sys/net/ipfw3_nat/ip_fw3_nat.c
Kernel: DragonFly 6.5-DEVELOPMENT #0 (master DEV, x86-64)
Verdict: REPRODUCED + FIX VALIDATED
The bug is real and confirmed by two independent lines of evidence:
-
Deterministic arithmetic proof β 1008 out of 64511 possible host-order alias port values (1.56%) produce out-of-bounds negative array indices
[-1020, -1], writing an 8-byte pointer up to 8160 bytes beforetcp_in[0]. -
Runtime state-table evidence β Driving 5000 UDP flows through ipfw3 NAT produced 102 TCP entries in the NAT state table (impossible with UDP-only traffic β these are UDP
s2structs misplaced intotcp_in[]by the byte-swap OOB). Every OOB entry's host-order port has low byte exactly 0β3.
Bug mechanism
In pick_alias_port() (line 439), the alias port is stored in network byte order:
s->alias_port = htons(krandom() % ALIAS_RANGE + ALIAS_BEGIN);
At lines 423/425 (same-CPU) and 721/723 (cross-CPU), the network-order value is used directly as an array index without ntohs():
alias->tcp_in[s->alias_port - ALIAS_BEGIN] = s2; // line 423
alias->udp_in[s->alias_port - ALIAS_BEGIN] = s2; // line 425
On little-endian x86-64, htons() byte-swaps the value. When the low byte of the host-order port is 0β3, the swapped value is < 1024, and swapped_value - ALIAS_BEGIN becomes negative (index range [-1020, -1]).
The read side (lines 204/209) uses the same byte-swapped index, so the connection "appears to work" while silently corrupting heap.
Byte-swap arithmetic table
| host_val | htons result | index = result β 1024 | OOB? |
|---|---|---|---|
| 0x0400 (1024) | 0x0004 (4) | β1020 | YES |
| 0x0401 (1025) | 0x0104 (260) | β764 | YES |
| 0x0402 (1026) | 0x0204 (516) | β508 | YES |
| 0x0403 (1027) | 0x0304 (772) | β252 | YES |
| 0x0500 (1280) | 0x0005 (5) | β1019 | YES |
| 0x0800 (2048) | 0x0008 (8) | β1016 | YES |
| 0x0404 (1028) | 0x0404 (1028) | 4 | no |
Probability: 1008/64511 = 1.5625% per new NAT'd connection.
Reproduction
Prerequisites
- Root access on DragonFlyBSD guest with ipfw3 modules available
kldload ipfw3 && kldload ipfw3_basic && kldload ipfw3_nat
Steps (automated by run.sh)
- Set
net.filters_default_to_accept=1(prevent SSH lockout) - Load ipfw3 modules
- Configure NAT:
ipfw3 nat 1 config ip <vtnet0_ip> - Add NAT rule:
ipfw3 add 100 nat 1 udp from any to any out - Drive 5000+ UDP flows:
./nat_oob_trigger 5000 10.0.2.2 - Check state table:
ipfw3 nat 1 show state | grep " tcp " | wc -l- Bug present: >0 TCP entries (OOB writes) - Fixed: 0 TCP entries
Build
./build.sh
Run
./run.sh 5000
Fix
Add ntohs() at every array index site where the network-byte-order alias_port is used as an index (8 sites total: 3 read, 5 write). See fix.diff.
Before/after comparison (5000 UDP flows): | Metric | Unpatched (#0) | Patched module | |--------|---------------|----------------| | TCP entries in state table | 102 | 0 | | OOB writes | ~102 | 0 | | All OOB entries low byte 0β3 | β | N/A |
Files
alias_port_oob_proof.cβ standalone arithmetic proofnat_oob_trigger.cβ UDP flow driver (primary trigger)nat_oob_trigger_tcp.cβ TCP connection driver (slow, for reference)raw_syn_trigger.cβ raw SYN packet sender (fast TCP path)run.shβ automated setup + driverbuild.shβ build scriptfix.diffβ git-apply-able fix (ntohs at all 8 index sites)VERDICT.mdβ detailed analysispanic.txtβ kernel panic from raw SYN (separate checksum bug)run_state_table.txtβ full state table (unpatched, 102 TCP entries)fix_run_state_table.txtβ full state table (patched, 0 TCP entries)
DF-0569 β VERDICT
Verdict: REPRODUCED + FIX VALIDATED
The heap OOB write via byte-swapped alias_port used as array index is real, confirmed, and fixable. Two independent lines of evidence prove the bug at runtime; the fix eliminates all OOB writes.
Root-cause mechanism (line-by-line)
The store (line 439)
// sys/net/ipfw3_nat/ip_fw3_nat.c:439
s->alias_port = htons(krandom() % ALIAS_RANGE + ALIAS_BEGIN);
pick_alias_port() generates a host-order port in [1024, 65534], then stores it as network byte order via htons(). On little-endian x86-64, this byte-swaps the two bytes.
The mis-indexed write (line 423, same-CPU path)
// sys/net/ipfw3_nat/ip_fw3_nat.c:423
alias->tcp_in[s->alias_port - ALIAS_BEGIN] = s2;
s->alias_port is read as a uint16_t β on little-endian, its VALUE is the byte-swapped result. This is used directly as an array index without ntohs(). The C integer promotion rules make s->alias_port - ALIAS_BEGIN a signed int. When the byte-swapped value < 1024, the result is negative.
Same bug at 7 more sites
- Line 425:
alias->udp_in[s->alias_port - ALIAS_BEGIN] = s2;(UDP write, same-CPU) - Line 721:
alias->tcp_in[msg->alias_port - ALIAS_BEGIN] = s2;(TCP write, cross-CPU) - Line 723:
alias->udp_in[msg->alias_port - ALANS_BEGIN] = s2;(UDP write, cross-CPU) - Line 204:
s2 = alias->tcp_in[*old_port - ALIAS_BEGIN];(TCP read, return path) - Line 209:
s2 = alias->udp_in[*old_port - ALIAS_BEGIN];(UDP read, return path) - Line 326:
alias->icmp_in[s->alias_port] = s2;(ICMP write) - Line 215:
s2 = alias->icmp_in[*old_port];(ICMP read)
When it fires
The OOB fires when the low byte of the host-order random port is 0, 1, 2, or 3 (probability 4/256 = 1.5625%). Example:
- host value 0x0401 (1025) β htons β 0x0104 (260) β index 260 - 1024 = -764
- Writes s2 (8-byte pointer) at tcp_in[-764], i.e., 6112 bytes before tcp_in[0]
OOB direction (correcting the finding claim)
The finding claims the index "wraps unsigned to ~64772, past tcp_in[64511]". This is incorrect β C integer promotion makes uint16_t - int β int, yielding a negative index in [-1020, -1]. The OOB write goes before the array, not past its end. The impact is the same (heap corruption), but the direction is opposite.
For the UDP path specifically, udp_in[] immediately follows tcp_in[] in struct cfg_alias, so udp_in[negative_index] writes into the tail of tcp_in[] β still wrong, but within the same allocation.
For the TCP path, tcp_in[negative_index] writes before the array: into struct cfg_alias's ip/next fields (indices -1 to -3) or into kernel heap before the allocation (indices -4 to -1020).
Evidence
1. Deterministic arithmetic proof (alias_port_oob_proof.c)
Scanned all 64511 possible host-order ports:
- 1008 values (1.56%) produce OOB indices
- OOB index range: [-1020, -1] (all negative)
- Maximum offset: 8160 bytes before tcp_in[0]
2. Runtime state-table evidence (5000 UDP flows through NAT)
After driving 5000 UDP flows through ipfw3 NAT on the unpatched #0 kernel:
| Metric | Value |
|---|---|
| Total NAT states | 15497 |
| UDP entries | 15395 |
| TCP entries | 102 β impossible with UDP-only traffic! |
All 102 TCP entries are UDP s2 structs misplaced into tcp_in[] by the byte-swap OOB. Every one has a host-order dst_port with low byte exactly 0β3:
| Low byte | Count |
|---|---|
| 0 | 27 |
| 1 | 25 |
| 2 | 26 |
| 3 | 24 |
| β₯4 | 0 |
This distribution is statistically perfect: uniform across {0,1,2,3} and zero outside β exactly the byte-swap OOB signature.
3. Panic (raw SYN packets)
Sending raw TCP SYN packets through the NAT caused a kernel panic:
delayed m_pullup, m->len: 40 off: 32084 p: 6 Fatal trap 12: page fault while in kernel mode fault virtual address = 0x10 Stopped at in_delayed_cksum+0x71: movq 0x10(%r12),%rax
Important caveat: This panic also occurs with the patched module (same crash signature). Investigation shows the crash is caused by the NAT code setting CSUM_TCP on raw packets whose mbuf metadata isn't set up for delayed checksum processing β a separate bug from DF-0569. The DF-0569 evidence is the UDP state-table OOB entries, not this crash.
Exploit chain (INVARIANTS hard blocker)
The primitive is an 8-byte pointer heap OOB write via the nat_state2 * store. On this guest, INVARIANTS is enabled, which blocks slab-grooming escalation:
- The write stores a valid
nat_state2 *pointer at a negative index. The pointer value is not attacker-controlled (it's a freshlykmalloc'dnat_state2), but the destination offset is determined by the random alias port's byte-swap. - For the TCP path (indices -4 to -1020), the write corrupts kernel heap before
cfg_alias. This could overwrite a victim object's function pointer,ucred *, refcount, or data pointer. With no SMAP/SMEP/KASLR, redirecting a corrupted pointer to userspace shellcode would giveuid=0. - However, INVARIANTS (enabled on this guest) catches many slab-level corruptions during allocation/free, making reliable slab grooming infeasible without first defeating the INVARIANTS checks.
- For the UDP path (indices -1 to -1020 from
udp_in, which maps into the tail oftcp_in[]), the write stays within thecfg_aliasstruct β no useful victim corruption.
Conclusion: The OOB write is a genuine memory-corruption primitive that could be escalated to uid=0 on a non-INVARIANTS kernel (the standard DragonFly release config). On this audit guest (INVARIANTS on), reliable escalation is blocked. The remote-trigger nature (any traffic through a NAT'd interface) makes this a high-severity finding regardless.
Fix validation
Fix: fix.diff
Adds ntohs() at all 8 array index sites, ensuring the host-order value is always used for indexing. The fix is minimal (8 single-line changes) and doesn't change the storage format of alias_port.
Before/after comparison (identical workload: 5000 UDP flows)
| Metric | Unpatched #0 kernel |
Patched module |
|---|---|---|
| TCP entries in state table | 102 (OOB writes confirmed) | 0 (no OOB) |
| OOB write rate | 1.56% of flows | 0% |
| All OOB ports have low byte 0β3 | β | N/A |
| Module hash | 28b50c63... | 400941d5... |
The fix completely eliminates the OOB writes. NAT continues to function correctly (9828 valid UDP states created and displayed).
Note on raw SYN crash
The raw SYN crash at in_delayed_cksum persists with the fix. This is a separate bug: the NAT code sets m->m_pkthdr.csum_flags = CSUM_TCP on raw packets whose mbuf metadata isn't properly initialized for delayed checksum processing. This is NOT DF-0569 and is not addressed by the byte-swap fix. It should be reported as a separate finding.
PoC changes
Authored the entire evidence pack from scratch (no seeded PoC folder existed):
- alias_port_oob_proof.c β deterministic arithmetic proof
- nat_oob_trigger.c β UDP flow driver (primary trigger, avoids SSH breakage)
- nat_oob_trigger_tcp.c β TCP connection driver (slow, for reference)
- raw_syn_trigger.c β raw SYN packet sender (fast TCP path, but hits separate checksum bug)
- run.sh β automated NAT setup + driver
- build.sh β build script
- fix.diff β 8-site ntohs() fix
Re-verification (2026-07-16, same #0 guest, 6 CPUs)
Re-confirmed on the current master DEV guest. After driving 4000 outbound UDP
flows the state table again showed the byte-swap OOB signature:
- 7812 UDP entries + 55 TCP entries (impossible with UDP-only traffic),
- every misplaced TCP entry's host-order dst_port has low byte exactly 0β3.
Additionally, under sustained NAT traffic the cleanup callout panicked on
the heap corrupted by the OOB writes (panic_cleanup.txt):
Fatal trap 12: page fault while in kernel mode fault virtual address = 0x1c7 Stopped at nat_cleanup_func_dispatch+0x1bf: subq 0x18(%rdi),%rdx
0x1c7 = s2(0x1af) + 0x18 = nat_state2->timestamp: the cleanup found a
garbage non-NULL nat_state2 pointer (0x1af) in one of the alias arrays
(corrupted by the OOB writes) and dereferenced it. This is direct runtime
proof the OOB writes corrupt adjacent heap and the corruption is fatal.
Fix verification
fixedVALIDATED via module hot-swap + diagnostic before/after.
BEFORE: 55-102 TCP entries from UDP-only -> panic. AFTER: 0 TCP entries.
Confirmed kernel references
Detail
Exploit chain
none -- root/KLD module or read-only or concurrency. See notes.
Evidence (decisive lines)
BEFORE: 55-102 TCP entries from UDP-only -> panic. AFTER: 0 TCP entries.
PoC changes
Various PoCs + fix.diff + VERDICT.md + manifest.json per finding.
Verified recommended fix
Add ntohs() at all 8 array-index sites (:204/:209/:215/:326/:423/:425/:721/:723). Full diff in findings/poc/DF-0569/fix.diff.
Verdict
REPRODUCED. pick_alias_port htons stores network order, used as array index without ntohs -> OOB write into tcp_in/udp_in. 4000 UDP flows -> 55 TCP entries (impossible) -> cleanup panic nat_cleanup_func_dispatch+0x1bf.
No comments yet.