cxm_tuner: terminator-less band_codes[3]/assignments[17] walks rely on unenforced min_freq/min_channel data invariants - latent rodata OOB-read hazard (Info hardening)
Summary
Both walks stay in bounds only because cross-field invariants hold in the static data (verified entry-by-entry for all 21 tuners and 9 channel sets in this tree); a future table edit breaking either turns an unpriv TVTUNER_SETCHNL/SETFREQ ioctl into a .rodata OOB read whose bytes are written to the I2C bus. Same latent class as DF-1681. Bound-by-extent + exhaustion-check fix.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-4540 Β· 10 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 2.9 KB | β raw | |
| VERDICT.md | β | 3.3 KB | β raw | |
| cxm_env_probe.sh | β | 585 B | view raw | |
| build.sh | β | 92 B | view raw | |
| run.sh | β | 118 B | view raw | |
| run.log | β | 515 B | view raw | |
| env.txt | β | 721 B | view raw | |
| fix.diff | β | 1.1 KB | view raw | |
| manifest.json | β | 954 B | view raw | |
| verdict.json | β | 3.6 KB | view raw |
DF-4540 β cxm_tuner: terminator-less tuner-table walks rely on unenforced cross-field data invariants (latent OOB-read hazard)
What this is (Info / hardening β no demonstrated impact with current tables)
Both tuner-table walks in cxm_tuner.c run without a terminator
element and without an array-extent bound, stopping only when an
element field drops to/below the request:
- Band-code walk β
cxm_tuner_select_frequency(), cxm_tuner.c:725-727:for (band_code = sc->tuner->band_codes; band_code->freq > freq; band_code++);struct cxm_tuner.band_codesis declared[3](cxm.h:619) and every tuner table incxm_tuners[]initializes all three slots (cxm_tuner.c:273-498) β there is no zero terminator slot at all. - Channel-assignment walk β
cxm_tuner_select_channel(), cxm_tuner.c:789-791:for (assignments = channels->assignments; assignments->channel > channel; assignments++);assignmentsis[17](cxm.h:594) andbg_air_channelsfills all 17 slots (cxm_tuner.c:112-128).
The only thing keeping both walks in bounds today are cross-field
invariants in the static data, checked at the call sites:
* freq >= sc->tuner->min_freq (cxm_tuner.c:706-708) together with
band_codes[2].freq == min_freq for all 21 tuner entries
(verified: cxm_tuner.c:273-498, e.g. FI1216 48250/48250,
Microtune 45750/45750, TCL 55250/55250);
* channel >= channels->min_channel (cxm_tuner.c:784-787) together
with the last assignment entry having channel == min_channel for
all 9 channel tables (cxm_tuner.c:72-251).
Neither invariant is enforced or asserted anywhere. A future table
entry that breaks either one (a new tuner whose min_freq is below its
last band-code freq, or a channel set whose last assignment channel is
above min_channel) makes an unprivileged TVTUNER_SETFREQ /
TVTUNER_SETCHNL ioctl (cxm.c:2824, 2864) walk off the end of a
.rodata array β an OOB read whose fetched bytes are then written to
the I2C bus (cxm_tuner.c:750).
This is the same class as the latent DF-1681 (unguarded divide, "latent") β a data-invariant dependency with no runtime guard.
Also checked and killed (see VERDICT.md)
msg[5] builders (in-bounds, tuner_msg_len β {4,5}), N scaling
(checked N > 32767), afcmap[16] index (masked & 0x0f), system-code
lookup (bounded NUM_ELEMENTS(codes[6]) + post-check), tuner-type
dispatch (bounds-checked, cxm_tuner.c:557), u32 kHz-conversion wrap in
cxm.c RADIO_SETFREQ/TVTUNER_SETFREQ (wraps are still range-checked
afterwards β no impact).
Build / run
./build.sh # n/a (shell probe) ./run.sh # documents no Conexant hardware/cxm device on this guest
Fix
fix.diff bounds both walks by array extent
(NUM_ELEMENTS(...)) and treats running off the end as -1 β the
invariant becomes enforced instead of assumed. git apply --check
clean; never applied to sys/.
DF-4540 VERDICT β untested (Info/hardening; hardware-gated), structure certain
The two walks (path:line)
cxm_tuner_select_frequency()TV branch: * guardfreq < min_freq || freq > max_freq β -1at cxm_tuner.c:706-708 * walkfor (band_code = sc->tuner->band_codes; band_code->freq > freq; band_code++);at cxm_tuner.c:725-727 *band_codes[3](cxm.h:619), fully initialized in all 21 entries ofcxm_tuners[](cxm_tuner.c:269-499) β no terminator slot exists. * Verified invariant: for every tuner,band_codes[2].freq == min_freq(FI1216/FM1216/FQ1216ME/ FQ1216ME MK3/FM1216ME MK3 48250; FI1236/FM1236/Temic 4036/4039/ TAPC-H701F/TAPC-H001F/TCL 55250; FI1246/FM1246/Temic 4066/ Microtune 45750; Temic 4006/4009/LG TPI8PSB11D/TPI8PSB01N/ TAPE-H001F 48250). Becausefreq >= min_freq, the walk always stops at index β€ 2. In bounds today only by this data invariant.cxm_tuner_select_channel(): * guardchannel < min_channel || channel > max_channel β -1at cxm_tuner.c:784-787 * walk at cxm_tuner.c:789-791,assignments[17](cxm.h:594);bg_air_channels(cxm_tuner.c:112-128) initializes all 17 slots β no terminator slot. * Verified invariant: the last assignment of each of the 9 channel tables haschannel == min_channel(us_air 2, us_cable 1, bg_air 2, bg_cable 2, bg_australia 1, i_air 1, l_air 1, japan_air 1, japan_cable 1) β so the walk stops at or before the final slot for every acceptedchannel.
If either invariant is broken by a future table edit, the walk runs off
the .rodata array: OOB read of successive 4/8-byte fields until some
garbage freq/channel compares low β the fetched band/control bytes
are then transmitted on the I2C bus (cxm_tuner.c:750-752). Reachable by
whichever user can open /dev/cxmN (bktr-style TVTUNER ioctls,
cxm.c:2819-2824/2856-2866).
Why it is Info, not higher
With the current 21 tuner entries and 9 channel tables the invariants hold universally (verified entry-by-entry above); no attacker input can drive either walk out of bounds today. The finding documents the unguarded dependency β the same "latent" class as DF-1681.
Verification status
Untested on the guest (and unnecessary for an Info hardening finding):
the cxm driver requires a Conexant CX2381x PCI function; the audit
guest has none (run.log: PCI-ABSENT: no vendor-14f1 function, no
/dev/cxm*), so the tuner paths cannot execute there. The invariant
verification above was performed statically against the tables in this
tree. Status untested, impact none, confidence certain
(about the structural facts).
Fix validation
fix.diff replaces the terminator assumption with explicit array-extent
bounds in both walks (NUM_ELEMENTS), returning -1 on exhaustion;
git apply --check clean. Not runtime-observable on this guest β
fix_status not_testable.
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_testablefix.diff adds explicit NUM_ELEMENTS bounds plus exhaustion checks to both walks in cxm_tuner.c; git apply --check clean. Runtime behaviour identical for the current tables (invariant holds), so baseline vs patched cannot differ observably even with hardware; fix_status not_testable on this guest.
findings/poc/DF-4540/fix.diff
Confirmed kernel references
Detail
Evidence (decisive lines)
findings/poc/DF-4540/VERDICT.md (entry-by-entry invariant verification); cxm_tuner.c:725-727 and cxm_tuner.c:789-791 walks; cxm.h:619 band_codes[3]; cxm.h:594 assignments[17]; guards cxm_tuner.c:706-708 / cxm_tuner.c:784-787; run.log/env.txt (no vendor-14f1 function, no /dev/cxm*)
PoC changes
authored fresh shell probe (no seed); documents trigger-hardware absence
Verified recommended fix
Bound both walks by array extent (NUM_ELEMENTS) and return -1 on exhaustion instead of relying on the min_freq/min_channel data invariants
Verdict
Both tuner-table walks in cxm_tuner.c have no terminator element and no array-extent bound: the band-code walk at cxm_tuner.c:725-727 iterates struct cxm_tuner.band_codes[3] (cxm.h:619) whose three slots are fully initialized in all 21 cxm_tuners[] entries (no terminator slot exists), and the channel-assignment walk at cxm_tuner.c:789-791 iterates assignments[17] (cxm.h:594) which bg_air_channels fully populates (cxm_tuner.c:112-128). They stay in bounds only because unenforced data invariants hold in the current tables: freq >= min_freq (cxm_tuner.c:706-708) combined with band_codes[2].freq == min_freq for all 21 entries, and channel >= min_channel (cxm_tuner.c:784-787) combined with the last assignment channel == min_channel for all 9 channel tables -- both verified entry-by-entry in this tree. A future table edit breaking either invariant turns an unprivileged TVTUNER_SETCHNL/TVTUNER_SETFREQ ioctl (cxm.c:2824/2864) into a .rodata OOB read whose bytes are then written to the I2C bus (cxm_tuner.c:750). No input can cross the bounds with the current tables, so Info/hardening, same latent class as DF-1681. Untested: cxm needs a Conexant CX2381x PCI function absent from the audit guest (run.log); the invariant verification was static.
No comments yet.