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

ixgbe_checksum_ptr_x550/ixgbe_calc_checksum_X550 caller-buffer path: bounds checks ignore the ptr base offset (plus two off-by-ones) β€” OOB read past the caller's u16 buffer[]

Summary

The exported caller-buffer path of the X550 NVM checksum API validates start+length against buffer_size while reading buffer[ptr+i], so a call that passes every shipped guard walks up to ptr words past the end of the caller's buffer; the ptr==buffer_size and buffer_size==EEPROM_LAST_WORD cases are additional off-by-one OOB reads. PROVEN by an on-guest replica of the exact guards/loop: 'SHIPPED: OOB READ - first out-of-bounds word index = 100' + 'FIXED: rejected (ERR_PARAM)'. Latent: the only in-tree caller passes NULL (the live NULL path is bounded and safe). Same defect verified in Linux master and FreeBSD head (shared-code bug). Fix compile-validated on guest module build -Werror clean. Any future caller handing calc_checksum a user-sized buffer turns this into a kernel OOB read whose 16-bit checksum return is a weak info-leak oracle.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3723 Β· 12 files
FileTypeDescriptionSize
README.md β€” 2.1 KB ↓ raw
VERDICT.md β€” 4.1 KB ↓ raw
chksum_bounds_demo.c β€” 5.5 KB view raw
build.sh β€” 254 B view raw
run.sh β€” 132 B view raw
build.log β€” 82 B view raw
run.log β€” 383 B view raw
fix_build.log β€” 10.5 KB view raw
env.txt β€” 1.5 KB view raw
fix.diff β€” 725 B view raw
manifest.json β€” 1.3 KB view raw
verdict.json β€” 2.9 KB view raw

DF-3723 β€” ixgbe_checksum_ptr_x550 caller-buffer path: bounds checks ignore the ptr base offset (+ two off-by-ones) β†’ OOB read past caller's u16 buffer[]

File: sys/dev/netif/ix/ixgbe_x550.c (lines 3338–3397, 3407–3433) Severity: Low (latent β€” no in-tree caller passes a non-NULL buffer) Confidence: certain (source-proven + on-guest bound-math demo)

Build / Run (bound-math demo)

The guest has no X550 NIC, so the HAL cannot run on hardware. The demo replicates the exact shipped guards and loop and shows a valid-looking call reading past the end of the caller's buffer; it also runs the fixed guards (the exact code in fix.diff) and shows them rejecting the same call.

# on the audit host:
ssh -F dfbsd-qemu/config dfbsd 'cat > /tmp/chksum_bounds_demo.c' < chksum_bounds_demo.c
dfbsd-qemu/vm.sh run_root 'cc -O2 -o /tmp/chksum_bounds_demo /tmp/chksum_bounds_demo.c && /tmp/chksum_bounds_demo'

Expected (observed, see run.log):

SHIPPED: OOB READ β€” first out-of-bounds word index = 100 (buffer holds 0..99); reads run to buffer[90+50]=buffer[140]
FIXED:   rejected (ERR_PARAM) β€” OOB prevented
OFF-BY-ONE (buffer_size == ptr == 90): SHIPPED ACCEPTS and reads buffer[90] (reads length at buffer[90] β€” one word past end)
OFF-BY-ONE: FIXED rejects

Fix compile-validation (see fix_build.log)

fix.diff was applied inside the guest's /usr/src/sys/dev/netif/ix/ copy (repo sys/ is read-only and was never touched) and the if_ix module rebuilt with the kernel warning set including -Werror:

cc ... -Werror ... -c /usr/src/sys/dev/netif/ix/ixgbe_x550.c
cc -Wl,--build-id=sha1 ... -r -o if_ix.ko ... ixgbe_x550.o     # make_rc=0

The patch was then reverted in the guest (md5 restored to baseline).

Artifacts

  • chksum_bounds_demo.c / build.log / run.log β€” on-guest bound-math demo
  • fix.diff β€” 3-hunk guard fix, git-apply-able against sys/dev/netif/ix/ixgbe_x550.c
  • fix_build.log β€” full module build with the fix applied (clean, -Werror)
  • env.txt β€” guest uname + pciconf (no X550 present) + compiler version
  • VERDICT.md, manifest.json, verdict.json
VERDICT.md
↓ download raw

DF-3723 β€” VERDICT

Status: inconclusive at runtime (no X550 hardware on the guest), source-certain, bound-math violation demonstrated on the guest, fix compiles clean.

Root cause (path:line into sys/dev/netif/ix/ixgbe_x550.c)

ixgbe_calc_checksum_X550(hw, buffer, buffer_size) is the exported shared-code API for checksumming a caller-held NVM image. In the buffer != NULL path its helper ixgbe_checksum_ptr_x550():

  1. ixgbe_x550.c:3358 β€” if (buffer_size < ptr) return IXGBE_ERR_PARAM; then :3360 local_buffer = &buffer[ptr]; and :3368 length = local_buffer[0];. When buffer_size == ptr, buffer[ptr] is exactly one word past the end β€” the check must be <= (off-by-one).
  2. ixgbe_x550.c:3376 β€” if (buffer && ((u32)start + (u32)length > buffer_size)) validates start + length against the buffer base, but the reads at :3394 are local_buffer[i] == buffer[ptr + i] for i ∈ [start, start+length). The ptr base offset is missing from the bound, so the walk can extend ptr words past the end. Concrete passing case: buffer_size=100, ptr=90, buffer[90]=50 β†’ guards pass (91 ≀ 100), loop reads buffer[91..140] β†’ 41 words OOB (demo output in run.log).
  3. ixgbe_x550.c:3430 β€” if (buffer_size < IXGBE_EEPROM_LAST_WORD) but the loop at :3439 is for (i = 0; i <= IXGBE_EEPROM_LAST_WORD; i++) β€” with buffer_size == 0x41 exactly, local_buffer[0x41] is one word OOB (second off-by-one).

DF-3670 family (NVM-walk bounds), X550 variant: the defect is confined to the caller-buffer path. The live path taken at attach time (if_ix.c:615 β†’ validate_eeprom_checksum β†’ calc_eeprom_checksum β†’ ixgbe_calc_checksum_X550(hw, NULL, 0)) is safe: the NVM-walk there is bounded by pointer >= hw->eeprom.word_size (:3454-3456) and (ptr + length) >= hw->eeprom.word_size (:3371-3373), and the 256-word stack chunk buffer (:3342) is correctly refilled (:3380-3393) β€” verified by trace, no OOB.

Why only Low

  • No in-tree caller passes a non-NULL buffer. The only caller is ixgbe_calc_eeprom_checksum_X550() (ixgbe_x550.c:3488-3491) which passes NULL, 0; ixgbe_api.c exposes no ixgbe_calc_checksum(hw, buf, size) wrapper in this tree, and if_ix.c never touches eeprom.ops.calc_checksum with a buffer. Dead code in the kernel as built (compiled in, never executed) β€” same posture as DF-1862 (latent).
  • Upstream-identical defect: verified 2026-09-15 against Linux master (drivers/net/ethernet/intel/ixgbe/ixgbe_x550.c, ixgbe_checksum_ptr_x550, same buffer_size < ptr and start + length > buffer_size) and FreeBSD head (sys/dev/ixgbe/ixgbe_x550.c:3325,3343). Not a DragonFly regression.
  • If a future caller (e.g. an NVM-tool ioctl or ported FreeBSD ixgbe_ndd/eeprom-dump sysctl) passes a user-sized buffer, this becomes a kernel OOB read whose 16-bit checksum return is a (weak) info-leak oracle over adjacent kernel memory.

Verification performed

Step Result
Guest hardware check (env.txt) no Intel X550/82598/82599 device on PCI β€” HAL paths cannot execute
On-guest bound-math demo (run.log) shipped guards accept ptr=90,len=50,bs=100 β†’ reads buffer[91..140]; buffer_size==ptr off-by-one accepted; fixed guards reject both
Fix compile (fix_build.log) fix.diff applied to guest /usr/src copy; full if_ix.ko module rebuild clean with -Werror; patch reverted after
Repo sys/ tree never modified (git clean)

fix.diff in this pack (3 hunks): - :3358 buffer_size < ptr β†’ buffer_size <= ptr - :3376 add (u32)ptr + to the bound - :3430 buffer_size < IXGBE_EEPROM_LAST_WORD β†’ < IXGBE_EEPROM_LAST_WORD + 1

Worth carrying upstream (Linux/FREEBSD share the bug).

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

inconclusive
baseline no→ patch + rebuild →patched clean

fix.diff applied to guest /usr/src/sys/dev/netif/ix; full if_ix.ko module rebuild clean with -Werror (fix_build.log, make_rc=0); harness shows fixed guards reject both the ptr-offset and off-by-one cases. Runtime fix validation impossible: no X550 hardware, and the buffer path has no in-tree caller. Patch reverted in guest afterwards (md5 restored).

fix_build.log: 'cc ... -Werror ... -c ixgbe_x550.c' + 'cc -r -o if_ix.ko ... ixgbe_x550.o' rc=0; run.log FIXED lines
↓ fix.diffper-fix-DF-3723

Confirmed kernel references

Detail

Evidence (decisive lines)

run.log (SHIPPED: OOB READ first OOB index=100; OFF-BY-ONE accepted vs FIXED rejected), fix_build.log (if_ix.ko rebuild with fix.diff clean under -Werror), env.txt (no X550 hardware on guest)

PoC changes

wrote bound-math harness replicating the exact shipped guards + loop (no hardware needed); fix.diff authored and compile-validated on guest /usr/src

Verified recommended fix

Include the ptr base offset in the bounds check ((u32)ptr+(u32)start+(u32)length > buffer_size), make :3358 use <=, and require buffer_size >= IXGBE_EEPROM_LAST_WORD+1 at :3430

Verdict

Source-certain OOB read in the exported caller-buffer path of ixgbe_calc_checksum_X550/ixgbe_checksum_ptr_x550: the guard at ixgbe_x550.c:3376 checks start+length against buffer_size but reads occur at buffer[ptr+i] (:3360,:3394), and :3358/:3430 are off-by-one β€” proven by an on-guest replica of the exact guards/loop (run.log: shipped code reads buffer[91..140] of a 100-word buffer; fixed guards reject). Runtime inconclusive only because the guest has no X550 NIC (env.txt: pciconf shows no Intel 10G device) and no in-tree caller passes a non-NULL buffer β€” latent, same defect present in Linux master and FreeBSD head. The live NULL-buffer path used at attach (if_ix.c:615) is bounded by word_size checks (:3454,:3371) and is safe.