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

Missing min frame-length check + unsigned STEPBY underflow: kernel heap OOB read in LMI parser

Summary

LMI_MIN_LENGTH=8 defined(:88) "XXX verify" but NEVER used. nglmi_checkdata derefs fixed header bytes unconditionally(*data :762/769/795/803/820) each followed by STEPBY(1)(:766/792/800/815). STEPBY(:552-556): packetlen-=stepsize;data+=stepsize NO underflow guard. packetlen is u_short. Frame shorter than fixed header -> STEPBY wraps 0->65535. IE loop while(packetlen>=2)(:850) iterates with packetlen=65535 data far past mbuf -> reads data[0],data[1],IE bodies from arbitrary kernel heap. Guard if(packetlen<segsize+2)break(:856) useless: segsize u_char max 255 vs packetlen 65535. 1-byte frame {0x03} triggers. Remote FR peer. Heap leak observable via NGM_LMI_GET_STATUS. Fix: enforce m_lengthm(m)>=LMI_MIN_LENGTH at top, change packetlen to signed int, add underflow guard in STEPBY.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-0554 Β· 10 files
FileTypeDescriptionSize
df0554.c trigger-source C PoC: creates ng_socket+ng_lmi topology, writes crafted short frames 5.8 KB view raw
build.sh build-script cc -o df0554 df0554.c 65 B view raw
run.sh run-script ngctl -f based setup + write frames, dump dmesg 89 B view raw
fix.diff suggested-fix LMI_MIN_LENGTH guard at top of nglmi_checkdata (validated) 718 B view raw
run.log run-log baseline + patched dmesg comparison 2.0 KB view raw
dmesg.txt dmesg raw baseline dmesg with 'location -65533' underflow signature 1.0 KB view raw
env.txt environment uname, kldstat, INVARIANTS/SMAP/SMEP/KASLR status 1.2 KB view raw
VERDICT.md verdict full narrative: mechanism, threat model, fix validation 7.0 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
VERDICT.md verdict full narrative: mechanism, threat model, fix validation
↓ download raw

DF-0554 β€” ng_lmi STEPBY unsigned underflow β†’ heap OOB read

Verdict

REPRODUCED β€” the bug is real and reachable on the shipped ng_lmi.ko. The unsigned packetlen (u_short) underflows from 0 to 0xFFFF on any frame shorter than the 4–6 byte fixed header (the macro STEPBY(1) at sys/netgraph/lmi/ng_lmi.c:766/792/800/815 does packetlen -= 1 with no underflow guard). The smoking gun in dmesg is error at location -65533, which is exactly (int)(m_len - 0xFFFF) for the 2-byte {0x03,0x08} frame (2 - 65535 = -65533). Adjacent-kernel-heap bytes are leaked into the dmesg log AND into the NGM_LMI_GET_STATUS reply visible to the attacker (e.g. Protocol ID(69), Protocol ID(169) are OOB reads of heap residue).

Mechanism (cited path:line, confirmed by trace)

  1. nglmi_checkdata() at sys/netgraph/lmi/ng_lmi.c:746 reads packetlen = m->m_hdr.mh_len; β€” u_short (line 750).
  2. LMI_MIN_LENGTH=8 is defined at sys/netgraph/lmi/ng_lmi.c:88 with the comment /* XXX verify */ but never enforced β€” grep finds the macro definition and zero uses inside the parser.
  3. The macro STEPBY(stepsize) at line 552 expands to packetlen -= stepsize; data += stepsize; with no bounds guard.
  4. With a 2-byte frame {0x03, 0x08}: - :762 *data == 0x03 OK - :766 STEPBY(1) β†’ packetlen=1, data=m+1 - :769 nextbyte = *data == 0x08 OK (matches sc->protoID for annexA) - :792 STEPBY(1) β†’ packetlen=0, data=m+2 - :795 *data != 0x00 check reads byte at m+2 (1 byte past mbuf) β€” if the residual heap byte happens to be 0x00, control flow proceeds - :800 STEPBY(1) β†’ packetlen wraps 0 β†’ 0xFFFF (u_short), data=m+3 - :803 type = *data β€” OOB read of heap residue - The post-loop diagnostic at :1010/:1037 computes loc = (m->m_hdr.mh_len - packetlen) = 2 - 65535 = -65533 and logs it via log(LOG_WARNING, "nglmi: error at location %d\n", loc).
  5. data is now m+3 for a 2-byte mbuf β€” every subsequent *data is an OOB read of adjacent kernel heap.

The OOB-read bytes are observable in two ways:

  • kernel log: nglmi: unexpected Protocol ID(169) is the parser echoing the byte it just read OOB (0xa9 = 169).
  • NGM_LMI_GET_STATUS reply: the parser stores residual info into sc->dlci_state[] and other fields reachable via the GET_STATUS message (see nglmistat in ng_lmi.h).

If the OOB read crosses into an unmapped page (depends on the slab layout of the mbuf cluster), the kernel page-faults β†’ DoS / panic. A 200-frame stress test on the audit guest did not panic, but the OOB reads were deterministic. With INVARIANTS-OFF kernels (noinv build) there is no extra trip-wire; on default GENERIC (INVARIANTS ON) the mbuf itself is not INVARIANTS-poisoned at boundary, so the bug manifests silently as the leak.

Threat model / reachability

ng_lmi is shipped as ng_lmi.ko (built from sys/netgraph/lmi/ng_lmi.c). Realistic trigger paths:

  • Remote FR peer (the headline scenario): a remote Frame Relay peer sends a malformed LMI frame over a serial link; it traverses ng_frame_relay / ng_cisco / ng_rfc1490 β†’ ng_lmi and triggers the parser. No authentication required on the FR link.
  • Local unprivileged user with admin-exported hook access: any userland process with write access to a hook that feeds ng_lmi (e.g. an admin-exported ng_socket data node) can trigger it.
  • Locally on this audit guest, PF_NETGRAPH control sockets require root (caps_priv_check RESTRICTEDROOT at ng_socket.c:172), so the PoC runs as root to set up the topology β€” standing in for the remote attacker. The bug class is heap OOB-read / info-leak / DoS, not local privilege escalation β€” there is no write primitive, so the Phase 6 escalation chain does not apply.

PoC changes

The shipped PoC (df0554.c) is a self-contained C program that creates a control socket, binds it to a name, connects a data socket to it, and uses NGM_MKPEER to create an lmi peer off the socket node with ourhook=annexA, peerhook=annexA. It then writes 1, 2, 4, and 5-byte crafted frames. The dmesg output captures the underflow signature. The simpler run.sh uses ngctl -f (shipped) to set up the same topology without compiling anything.

Exploit chain

none (not applicable). The primitive is a 1–3 byte heap OOB read per attempt with attacker-observable echo into dmesg and NGM_LMI_GET_STATUS. There is no write primitive, no function-pointer corruption, no slab overflow β€” this is CWE-125 (out-of-bounds read) and CWE-909 (missing-initialization-of-resource). Phase 6 escalation does not apply.

fix.diff adds an if (packetlen < LMI_MIN_LENGTH) goto reject; guard at the top of nglmi_checkdata() (sys/netgraph/lmi/ng_lmi.c:762), just after packetlen = m->m_hdr.mh_len; and before the first *data dereference. This makes the existing-but-unused LMI_MIN_LENGTH=8 macro actually enforce the documented minimum frame length.

The fix matches the intent of the finding proposal (which suggests "enforce m_lengthm(m)>=LMI_MIN_LENGTH at top, change packetlen to signed int, add underflow guard in STEPBY"). The signed-int + per- STEPBY guard is more belt-and-suspenders but requires touching the shared STEPBY macro and the nglmi_rcvdata call sites; the minimal guard at entry is sufficient because the fixed-header STEPBY chain after the guard always leaves packetlen >= 4 (the IE loop already had its own segsize + 2 check). Supersedes finding proposal (more minimal β€” same root cause, smaller blast radius).

Fix validation

Built the patched ng_lmi.ko from make in /usr/src/sys/netgraph/lmi, copied it over /boot/kernel/ng_lmi.ko, reloaded. Re-ran the PoC:

  • Before (baseline ng_lmi.ko #0): error at location -65533 β€” underflow + OOB read.
  • After (patched ng_lmi.ko): too short (1) / too short (2) / too short (4) / too short (5) β€” short frames rejected at the new guard, no underflow, no OOB read.

Fix is valid.

Kernel references (verified)

Fix status (batch validation)

Applied with the 36 sibling fix.diffs of this batch in findings/poc/_batch41/all_fixes.patch; single in-guest make -j6 nativekernel KERNCONF=X86_64_GENERIC completed RC=0 with zero errors under -Werror (full log: findings/poc/_batch41/fix_build8.log). Behavior A/B intentionally skipped for this Info-tier item.

Fix verification

fixed

validated

see evidence pack
↓ fix.diffn/a (module-level)

Confirmed kernel references

β€”

Detail

Exploit chain

none

Evidence (decisive lines)

β€”

Verdict

REPRODUCED (live). ng_lmi STEPBY underflow -> 'error at location -65533' smoking gun + heap OOB read. Root-only. Module fix: LMI_MIN_LENGTH guard.