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

rt2560_set_chan walks the unterminated RT2560_RF5222 channel table with no bound β€” OOB read across kernel .rodata and garbage RF programming when an admin-installed channel misses the table

Summary

'for (i = 0; rt2560_rf5222[i].chan != chan; i++);' has no loop bound and the 41-entry table has no terminator row. Stock channel lists are fully covered, but the REGDOMAIN ioctl memcpy's an admin-supplied channel table into ic_channels (validates only freq/flags/maxregpower) - selecting an installed channel whose ieee number is outside the table walks off into adjacent kernel .rodata until a byte coincidentally equals chan, then programs the following 12 bytes into RF R1/R2/R4. Privileged local user -> OOB read (data goes into RF registers, no disclosure path) + radio misprogramming. Bound-the-walk fix (module rebuilt -Werror clean).

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3970 Β· 11 files
FileTypeDescriptionSize
README.md β€” 1.8 KB ↓ raw
VERDICT.md β€” 2.3 KB ↓ raw
trigger.c β€” 2.1 KB view raw
build.sh β€” 67 B view raw
run.sh β€” 91 B view raw
run.log β€” 317 B view raw
env.txt β€” 317 B view raw
fix.diff β€” 523 B view raw
build.log β€” 37.9 KB view raw
manifest.json β€” 650 B view raw
verdict.json β€” 3.3 KB view raw

DF-3970 β€” rt2560_set_chan: unbounded walk of the unterminated RT2560_RF5222 channel table β†’ OOB read + misprogrammed RF (admin-installed channel)

File: sys/dev/netif/ral/rt2560.c (pass 2, GLM 5.3)

What

rt2560_set_chan (2210) does for (i = 0; rt2560_rf5222[i].chan != chan; i++); with no bound against a 41-entry table (rt2560reg.h:449-489) that has no terminator. The stock channel lists (ieee80211_regdomain.c:101-108) are fully covered by the table, so the default configuration never misses β€” but SIOCS80211/IEEE80211_IOC_REGDOMAIN (ieee80211_ioctl.c:2126-2162 β†’ ieee80211_setregdomain β†’ the driver's null_setregdomain, ieee80211_regdomain.c:59-65, accepts anything) memcpy's an admin-supplied channel table into ic->ic_channels validating only freq/flags/maxregpower β€” never the ieee number against the driver's RF tables. Selecting an installed channel whose ieee number is not among the 41 entries (165, 34, 184-196, …) walks the loop off the end of the table into adjacent kernel .rodata until a byte coincidentally equals chan, then programs whatever 12 bytes follow into RF registers R1/R2/R4.

Impact

Kernel OOB read (bounded by kernel image mapping; data not returned to userspace) + garbage RF programming β†’ radio misbehavior / potential hang. Admin-gated (regdomain ioctl + channel set are privileged), Low.

Build / Run

Hardware-gated (RT2560/RT5222 dual-band NIC; guest has none β€” env.txt): cc trigger.c (informational) # on target as root: install a regdomain channel table containing ieee=165, # then ifconfig wlan0 channel 165

Fix validation

fix.diff bounds the walk with nitems() and logs+bails on unknown channels; git apply --check clean; module rebuilt clean with -Werror on the guest (build.log).

VERDICT.md
↓ download raw

DF-3970 verdict β€” certain unbounded table walk; default-config-safe, admin-gated trigger

Reproduced?

No Ralink NIC on the audit guest (env.txt), and the trigger additionally needs a privileged regdomain install β€” so not exercised at runtime. Source-level certainty is complete:

  • rt2560.c:2210: for (i = 0; rt2560_rf5222[i].chan != chan; i++); β€” no loop bound; i is u_int, the walk can run to the end of the kernel image if no byte ever equals chan (chanβ‰₯1 always, so a zero byte terminates it β€” in practice within a few KB, still past the end of the 41-entry table).
  • rt2560reg.h:449-489: RT2560_RF5222 table has no {0,…} terminator row.
  • Safe-by-accident today: ieee80211_init_channels (ieee80211_regdomain.c:101-108) builds exactly {1..14, 36..64 step 4, 100..140 step 4, 149,153,157,161} β€” all present in the table. ic_curchan always comes from ic_channels (ieee80211_set_channel / setupcurchan), so out-of-the-box the walk always terminates in-bounds.
  • Reachable channel numbers outside the table: IEEE80211_IOC_REGDOMAIN (ieee80211_ioctl.c:2126-2162) copies an admin channel array in and only validates freq/flags/maxregpower (ieee80211_regdomain.c:342+); the ral driver's ic_setregdomain is null_setregdomain (regdomain.c:59-65) which accepts anything, and the table is memcpy'd into ic->ic_channels (regdomain.c, "channel table" commit block). ieee=165/34/184… then select cleanly via ifconfig wlan0 channel N.

Impact ceiling: OOB read over kernel .rodata (no disclosure channel β€” the read values go into RF registers, not to userspace) + wrong RF programming (radio DoS). Admin-only trigger β‡’ Low.

Fix

fix.diff bounds the walk with i < nitems(rt2560_rf5222) and on miss logs "unknown channel %u for RF5222" and breaks out (BBP path still runs, RF left untouched β€” same behavior as the unknown-rf_rev default arm at rt2560.c:2217-2218). git apply --check clean; if_ral.ko rebuilt with -Werror on the guest (build.log). Runtime validation needs the NIC.

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

not_testable
baseline no→ patch + rebuild →patched clean

nitems() bound + 'unknown channel %u for RF5222' log-and-break; git apply --check clean against sys/ and the patched module rebuilt -Werror clean (build.log; combined with the other two rt2560 fix.diffs of this batch). Runtime validation impossible without the NIC.

fix.diff; build.log (if_ral.ko linked from patched rt2560.o with -Werror on the guest /usr/src copy)
↓ fix.diffper-fix-DF-3970

Confirmed kernel references

Detail

Exploit chain

root installs custom regdomain channel table containing e.g. ieee=165 -> 'ifconfig wlan0 channel 165' -> ic_curchan->ic_ieee=165 not present in rt2560_rf5222[] -> unbounded walk past table end in kernel .rodata -> OOB read + garbage RF programming (radio DoS).

Evidence (decisive lines)

VERDICT.md (walk + table + regdomain ioctl chain with path:line), trigger.c (on-target recipe), env.txt, fix.diff (nitems() bound + 'unknown channel' bail), build.log (if_ral.ko rebuilt -Werror clean on the guest /usr/src copy)

PoC changes

Trigger is a documented privileged-config recipe (needs NIC); informational C stub compiled only.

Verified recommended fix

Bound the RF5222 walk with nitems() and bail on unknown channel (fix.diff).

Verdict

Certain unbounded table walk: rt2560.c:2210 'for (i = 0; rt2560_rf5222[i].chan != chan; i++);' scans the 41-entry unterminated RT2560_RF5222 table (rt2560reg.h:449-489) with no loop bound. Safe by accident on stock configs because the default channel lists (ieee80211_regdomain.c:101-108) are fully covered, but SIOCS80211/IEEE80211_IOC_REGDOMAIN installs an admin channel table into ic->ic_channels with only freq/flags/maxregpower validated (ieee80211_regdomain.c:342+; the ral driver's null_setregdomain at regdomain.c:59-65 accepts anything), so selecting an installed channel whose ieee number is outside the table (165, 34, 184-196) walks off the end of .rodata until a coincidental byte match and programs garbage into RF R1/R2/R4. OOB read with no userspace disclosure path + radio misbehavior; admin-gated. Not reproduced: no Ralink NIC on the guest (env.txt) and the trigger needs a privileged regdomain install; recipe in trigger.c.