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)
PoC verification
Evidence pack
findings/poc/DF-3970 Β· 11 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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).
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;iis u_int, the walk can run to the end of the kernel image if no byte ever equalschan(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_RF5222table 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_curchanalways 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_testablenitems() 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)
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.
No comments yet.