mt7612u: access-point support — hardware beacon behind IRadio, open and WPA2-PSK verified on air - #424
Conversation
PR Summary by QodoAdd MT7612U hardware-beacon access point support
AI Description
Diagram
High-Level Assessment
Files changed (17)
|
Code Review by Qodo
1. Access points report ready without airing
|
Gap analysis, no code. The AP brain (probe/auth/assoc, DHCP/ARP/ICMP, WPA2 4-way) already exists in tests/ and is backend-agnostic, so the work is a few hundred lines of C backend primitives: StartBeacon (load the reserved page + arm the MAC beacon function), StopBeacon, per-station/GTK key install (hardware CCMP - a gain over Realtek's software CCMP), an AP RX filter, and confirming the auto-ACK covers SetAckResponder. Addressing, the station table, crypto slots, ACKed TX and the beacon timer are already present. Limitations named with workarounds: USB has no pre-TBTT interrupt so dynamic TIM/power-save is the one hard case (static beacon sidesteps it entirely for always-on FPV clients); BlockAck RX reordering and per-station rate control are software (decline BA / fixed-or-RSSI rate); multi-client is harness work. Verification reuses the existing beacon_*/ap_* harnesses. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Tba83kymS5W2v1vn2yRxrj
…ged subtree The MediaTek AP driver layer: a beacon the MAC auto-transmits from its reserved page at every TBTT, and the address match that makes it an AP rather than a beacon generator. mt_beacon_init / _write / _set_enable the beacon engine and slot 0 mt_ap_set_bssid the APC slot the MAC matches a BSS in regs.h MBSS mask correction, beacon regs tx.cpp MT_TXOPT_BEACON: hardware TSF + seq bringup: `beacon`, `ap` the two gates that measured it Device-verified 2026-09-08 (docs/mt7612u-ap-mode.md): beacon on air on both bands by a kernel station's `iw scan` and an independent RTL8812AU witness; timestamp advancing exactly 102400 us per beacon, so the MAC is inserting the live TSF; sequence +1 per beacon; and a real station's three auth frames arriving with 0 retried, which is the hardware auto-ACK - an un-ACKed frame comes back with FC Retry set. BCN_BYPASS_MASK is INVERTED: a set bit SUPPRESSES that slot. mt_beacon_init sets all sixteen and mt_beacon_write clears the one it loaded. Getting that backwards gives a running beacon timer, an advancing TSF, and nothing on air. Squashed with its own post-merge repair, because the four original commits predate the subtree's C++ migration (OpenIPC#421) and do not build on today's master: beacon.c was compiled by NOTHING (the Makefile globs *.cpp and the CMake list is explicit), ap_ctx used C11 _Atomic and the C11 free functions, ap_cb took its cookie through implicit void* conversions, and a memset ran over a no-longer-trivially-copyable type. Kept as one commit so the series bisects. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Tba83kymS5W2v1vn2yRxrj
The AP work was device-verified as bring-up gates driving the C library directly. Nothing reached it through IRadio, so pointing devourer's own AP harnesses at a MediaTek adapter beaconed nothing: StartBeacon fell through to the base class's `return false`. That was the whole gap. tests/ap_responder.cpp and tests/ap_wpa2.cpp already take an IRadio* - OpenIPC#415 made them genuinely backend-agnostic - and SetAckResponder, ReadTsf and WriteTsf were already implemented here. What was missing was three methods over primitives that were already proven on air. Three public entry points (31 now, api_link updated), because the backend is meant to reach the subtree through the public header and not through internal.h: mt7612u_beacon_start radiotap-framed or bare MPDU; publishes addr3 in APC slot 0, loads the reserved page, arms TBTT mt7612u_beacon_update reload in place, engine untouched mt7612u_beacon_stop clear the timer bits Two configurations are REFUSED rather than half-served, because each airs a beacon that scans perfectly and then ACKs nothing - the operator ends up debugging the RF link instead of the configuration: - a BSSID that is not the adapter's own MAC. The MAC ACKs against MT_MAC_ADDR and this call does not retarget it. - a locally-administered adapter MAC. Under MBSS_MODE=3 the hardware derives the BSS index from the address bits (mt76: 1 + n), so slot 0 is the wrong slot and the match would silently never fire. The Stage B gate already refused this; now the library does. Stop() silences the beacon before it releases the device. The MAC beacons autonomously once armed, so a beacon outliving this object airs until the adapter is power-cycled and contaminates the next run on that channel. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Tba83kymS5W2v1vn2yRxrj
Review of the previous commit found a genuine correctness bug in the new
public ABI and seven smaller ones. The reproduced one first:
BARE-MPDU BEACONS AIRED WITH AN UNINITIALIZED RATE WORD. beacon_split()
assigned five of mt7612u_tx_rate's nine fields; sgi, ldpc, stbc and
power_adj were left indeterminate. All four are read downstream -
mt_tx_rate_word() folds the first three into the 16-bit word the MAC
transmits VERBATIM, and a non-zero power_adj short-circuits the derived
per-rate TX power in mt_tx_build(). tests/ap_responder.cpp hands us a bare
MPDU, so this was the live path, and my own hardware run went out with
whatever was on the stack. The radiotap branch escaped only because
mt_radiotap_parse() memsets its output. Zero-initialised now.
- mt_radiotap_parse() has THREE return classes and beacon_split saw two:
a NEGATIVE return means "this IS a radiotap header and it is
malformed". Swallowing it parsed the radiotap bytes as an 802.11
header and read the BSSID out of the middle of it. Both sibling entry
points refuse on <= 0; so does this now.
- no_ack is forced, not taken from the caller's radiotap. Cleared, it
becomes MT_TXWI_ACK_CTL_REQ - an ACK request on a broadcast beacon.
Both bring-up gates hardcode it; the ABI was the weaker path.
- the header length is checked. mt_beacon_write() documents that it
relies on an unpadded 24-byte header; a QoS-data or 4-address frame
passed every check and would have landed in the reserved page as
[TXWI][hdr][2 pad][body].
- _beacon_active was not failure-atomic. beacon_start() disarms the
engine before it re-arms, so a failed RE-arm left the beacon dead
while the flag still reported the previous one live - which is how
UpdateBeaconPayload came to return true for every write into a
disarmed engine, the exact fault its guard exists to prevent.
- StopBeacon() cleared _beacon_active on FAILURE, so a caller retrying
after a transient stall was told "no beacon was active" and walked
away from one the MAC was still airing. It stays active now.
- the bypass-mask unsuppress is mt_wr_chk, not mt_wr. That single write
decides whether the beacon airs at all, and mt_ap_set_bssid twenty
lines below already argues why USB writes get checked and MMIO ones
do not.
- hw_beacon_txtsf said false next to the function that implements it.
The branch's own evidence is 102400 us per beacon.
- a memset over mt7612u_dev survived in frame_shape.cpp - added by this
branch, missed by the migration commit that claimed to have fixed it.
Invisible to CI, which never compiles src/mt7612u/tests/.
StopBeacon now retracts the WHOLE identity rather than half of it: the
APC BSSID slots are zeroed and, when beacon_start was what retargeted the
port MAC, that is restored too. It tracks who took it (beacon_took_identity)
so it cannot disarm an ACK responder the caller owns. The bring-up gate
already did this; the public path was weaker than the gate it claims to
reproduce.
Re-verified on hardware after the changes: beacon on air, a real station
associated, AUTH and ASSOC both at retry=0, 6/6 pings at 1.1 ms.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Tba83kymS5W2v1vn2yRxrj
I reported "the beacon was gone after the process exited" as evidence that StopBeacon works. It was not evidence of anything. Both AP harnesses end in `_exit(0)` (tests/ap_responder.cpp:end), which bypasses every destructor - so the radio's Stop(), and with it StopBeacon(), never ran in any of those runs. The beacon that was "gone" in one run was still airing in another, confirmed live at `last seen: 308 ms ago`, -32 dBm. The MAC beacons autonomously from the reserved page; nothing was silencing it. So: a harness that drives the transitions explicitly and lets an external station be the witness. PHASE 1 armed - scan MUST see the SSID -> seen PHASE 2 stopped - scan MUST NOT -> gone PHASE 3 re-armed - scan MUST see it again -> seen then an explicit stop -> gone `iw scan` alone is not the witness - its BSS cache holds an entry for ~30 s after the beacon dies, which reported a stopped beacon as present. `iw scan flush` is what distinguishes the two, and a phase-3 scan at +8 s still misses it: StartBeacon copies a 1600-byte page over EP0 and reads back the identity, so the re-arm is not instant. Both of those cost a false reading before they were understood, and the file says so. It also holds the two contract points that need no radio: a second StopBeacon returns false (no beacon is active), and UpdateBeaconPayload with nothing armed refuses instead of reporting success for a write into a disarmed engine. Not a ctest cell - it needs an adapter and a second radio to look. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Tba83kymS5W2v1vn2yRxrj
The status block said Stages C-E "still need the IRtlDevice wrapper". Stale twice over: the interface was renamed to IRadio in OpenIPC#415, and the wrapper now exists and is verified end to end. Every IRtlDevice mention is fixed, and RtlMt7612uDevice is Mt7612uRadio. Added the IRadio-path evidence as its own section, separate from the bring-up gates' - they are different code and only one of them is what a consumer reaches. With its counterparts: WPA2 unrun, the station is the same silicon, 70 s longest run, near-field. Two of those counterparts cost me a false reading each and are worth the space: `iw scan` caches a BSS for ~30 s so it reports a stopped beacon as present, and neither AP harness silences the beacon on exit because both `_exit(0)` past the destructor. The "gap - the driver primitives to add" section keeps its pre-migration .c filenames. The reasoning there is still correct and rewriting the citations would be churn; the header now says not to expect them to resolve. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Tba83kymS5W2v1vn2yRxrj
tests/ap_wpa2.cpp, unmodified, against an MT7612U: the 4-way handshake completes with a verified msg2 MIC, GTK delivered in msg3, station keyed at msg4 - and 6/6 pings at 1.156 ms over the encrypted link afterwards. It needs the same four IRadio methods as the open-network harness and no others, so nothing further was required of the backend. Two counterparts recorded rather than glossed. The encryption was not independently captured: no third radio sniffed the Protected bit, and wpa_cli had no control socket to report the negotiated cipher, so "encrypted" rests on the verified MIC plus traffic reaching a CCMP-only station. And this is devourer's SOFTWARE CCMP - the same code the Realtek backends use. MT_WCID_KEY / MT_SKEY are still untouched, so the doc's claim that crypto becomes hardware on this part is still a claim. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Tba83kymS5W2v1vn2yRxrj
ap_responder, ap_wpa2 and ul_trigger_ap all arm a hardware beacon and
then end in `_exit(0)`, which skips every destructor - so Stop(), and
with it StopBeacon(), never ran. The chip beacons AUTONOMOUSLY once
armed, so the beacon outlived the process and kept airing until the
adapter was power-cycled, contaminating whatever ran next on that
channel. Measured on MT7612U: a scan after the process was gone still
found the SSID live at `last seen: 308 ms ago`, -32 dBm.
IRadio.h has warned about exactly this since it was written ("killing
the host process does NOT silence it - bench-bitten"), and
beacon_update_probe.cpp already called StopBeacon before its own
`_exit`. These three did not.
One line each, before the exit, keeping the fast exit these harnesses
want. Realtek has the same exposure - the beacon is hardware-autonomous
on those parts too - so this is not a MediaTek fix.
Verified on hardware, MT7612U, ch36, station scanning with `iw scan
flush`:
ap_responder beacon up during the run, 0 after exit (was 1, live)
ap_wpa2 beacon up during the run, 0 after exit
`iw scan` without `flush` is not a witness here: its BSS cache holds an
entry ~30 s after the beacon dies and reports a stopped beacon as
present.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Tba83kymS5W2v1vn2yRxrj
Every number in docs/mt7612u-ap-mode.md's "Verified through IRadio"
table was hand-run, and three of the readings were wrong. This is those
steps as a script, with the three traps that produced the wrong readings
written into it:
- `iw scan` reports a beacon that stopped up to ~30 s ago as still
present, out of its BSS cache. Every check here uses `scan flush`.
- a bring-up that FAILS beacons nothing, so "no beacon" read as a pass
when the AP had never started. Each cell proves the AP came up before
it will believe an absence.
- the harnesses used to leave a beacon airing on exit, so a stale one
poisons the next cell. Cleanup power-cycles the port between cells.
Three cells, each with an external witness (a second MT7612U on the
kernel mt76x2u driver, told apart by sysfs id since the two share a PID):
open ap_responder beacon -> scan, associate, ping, auth retry=0
wpa2 ap_wpa2 beacon -> scan, 4-way, encrypted ping
stop beacon_stop_check armed -> stopped -> re-armed
Writing it down immediately caught a flaw in itself that the hand runs
had not: the stop cell slept a guessed 8 s after the phase banner before
scanning, and reported "armed but not scannable". The banner prints
BEFORE StartBeacon, and the arm is not instant - it copies a 1600-byte
page over EP0 and reads the identity back. Phase 3 passed only because it
happened to sleep 14 s. Both now wait for the log line that proves the
arm happened, counting occurrences so the re-arm waits for the second
one.
Then a second self-inflicted one, worth recording because it is a bash
trap and not a hardware one: `grep -c` PRINTS 0 and EXITS 1 when it
matches nothing, so `grep -c ... || echo 0` emits "0\n0" and every later
integer test dies on it.
Result on the bench: open 6/6, wpa2 4/4, stop 4/4.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Tba83kymS5W2v1vn2yRxrj
Four blockers from an adversarial pre-PR review. The first is mine and the
bench could not have caught it.
THE APC SLOT INDEX. beacon_start derived it as (ta[0] & 2) ? 1 : 0, arguing
mt76's `1 + (((macaddr[0] ^ addr[0]) >> 2) & 7)` collapses because the
identity had just been retargeted to `ta`. It does not collapse here.
dev->macaddr is written ONCE, from the EEPROM (eeprom.cpp), and
MT_MAC_BSSID_DW0/DW1 - the register that actually carries MBSS_MODE, and the
base the hardware derives the index from - is written once at init from the
factory MAC. mt7612u_set_ack_responder moves only MT_MAC_ADDR. So the
hardware was deriving its index from a different address than the host
assumed, and the constant 1 was right only for adapters where
1 + (((factory[0] ^ ta[0]) >> 2) & 7) happens to be 1.
This bench is one of them: factory 40:a5:ef:.., 0x40 ^ 0x02 >> 2 & 7 == 0. So
every on-air run agreed with the wrong reasoning. On an adapter whose first
byte is 0xe8 the AP would beacon perfectly and acknowledge nobody - the exact
failure the header warns about. The bring-up gate already refused that case;
the public ABI had flattened it to a constant.
Fixed by making the retarget a real setaddr: mt_mac_set_bss_base() moves
MT_MAC_BSSID with MT_MAC_ADDR, as mt76x02_mac_setaddr() does, so the premise
is true by construction rather than by luck. Restored on stop.
STOP COULD NOT FAIL. mt_beacon_set_enable's off path is mt_clear -> mt_rmw,
which reports only its READ half, and the mt_ap_set_bssid returns were
dropped - so mt7612u_beacon_stop returned non-zero only for a NULL device.
That made StopBeacon's whole failure branch unreachable, and the new
harness's assertion for it vacuous, while the real hazard (an EP0 stall
during teardown leaving the MAC beaconing) reported success. Bracketed with
the io_err delta.
STOP()'S PROMISED RETRY DID NOT EXIST. It called StopBeacon once and
discarded the result while the comment justified keeping _beacon_active on
the strength of a retry. Three attempts now, then an error naming what was
left airing.
THE HEADER DOCUMENTED THE OPPOSITE OF THE CODE. mt7612u.h still described
the pre-review behaviour ("deliberately does NOT restore it") after the code
started restoring, and omitted every refusal the code enforces. Rewritten
against the implementation.
Also:
- the reserved-page copy was the one write in mt_beacon_write that could
fail silently; mt_wr_copy is void and gives up mid-loop, leaving a HALF
WRITTEN beacon that then airs. Checked.
- UpdateBeaconPayload overwrote a live unsuppressed slot. mt76 brackets the
same write with BCN_BYPASS_MASK ("Prevent corrupt transmissions during
update"); without it a TBTT mid-copy airs a torn beacon.
- AdjustBeaconTiming / ...Fine / PinBeaconTbtt fell through to IRadio's
default 0, which reads as "applied a 0 us shift" rather than "not
implemented" - the one set of knobs on this backend that did not refuse
loudly.
- the slot bound admitted 12 bytes mt_tx_build then rejected with a
misleading error.
Docs: dropped a StopBeacon evidence row that the same document retracts two
sections later and that predates the function; corrected the _exit(0)
paragraph this branch made false; the WPA2 harness needs five IRadio methods,
not four, and StopBeacon has nothing to do with the cipher path; and a
"verified" row cited MT_WCID_KEY, which does not exist in this tree.
Harness: seen() took one scan as authoritative. A scan can miss a 100 TU
beacon, and it did - "beacon not scannable" in a run where the station then
associated, pinged and got an auth at retry=0. Three tries, highest count,
which is the conservative reading for both "present" and "gone".
Re-verified: open 6/6, wpa2 4/4, stop 4/4.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Tba83kymS5W2v1vn2yRxrj
Two came straight out of the previous round's changes, which is the useful kind: _beacon_active was cleared BEFORE the start call, to make it failure-atomic. But a start that fails before touching the engine - bad input, a failed identity retarget - then leaves the PREVIOUS beacon on the air while update, stop and the destructor all read it as inactive. An orphaned live beacon, which is the one outcome this whole change exists to prevent. mt7612u_beacon_start now returns three outcomes: 0 armed, -1 refused without touching hardware (flag unchanged, whatever was airing still is), -2 failed after the engine was disarmed and the library unwound the rest. beacon_start never checked io_err. I added that to stop last round and not to start, so a failed EP0 transfer while arming returned success - an AP that reports ready and airs nothing. Three more on the identity register, which has two owners and no arbitration: a failed start left it retargeted with no beacon, so the adapter answered for a BSS that does not exist. Every failure path unwinds now. a SetAckResponder issued AFTER StartBeacon was silently disarmed by the matching StopBeacon putting the factory address back. Ownership transfers to whoever wrote last. Two input-classification holes, both of which could put attacker- or caller-controlled bytes into the autonomous beacon page: mt_radiotap_parse returns 0 for "not a radiotap header" AND for "is one and it is malformed", and rlen == len (a header with no frame after it) fell to the bare-MPDU branch too. Byte 0 decides the shape now - a beacon's frame control is 0x80, radiotap's version must be 0 - and each shape is held to its own rules with no fallback to the other. mt7612u_beacon_update raised its suppression guard with an unchecked mt_wr. If that write never lands the copy runs against a live slot and a TBTT mid-copy airs a torn beacon, which is the single thing the guard is for. And four in the on-air harness, which matter because it runs as root: it wrote `authorized` to a caller-supplied sysfs path without checking what was on it - a stale AP_SYSFS would yank an unrelated device. Confirmed against 0e8d:7612 first. `pkill -x wpa_supplicant` would drop every wireless client on the host, and a name kill reaches a concurrent run of this same test. It tracks the PIDs it started. seen() matched SSID alone, so a neighbour running "devourerAP" could pass an arm check or fail a stop check. Matched on BSSID too. the harnesses called StopBeacon once and ignored the result immediately before _exit - and IRadio.h now says such a failure "must be retried". Three attempts, then a warning naming what is still airing. Re-verified on hardware: 14 passed, 0 failed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Tba83kymS5W2v1vn2yRxrj
7b1e446 to
424170b
Compare
josephnef
left a comment
There was a problem hiding this comment.
Reviewed at 424170b. Built it (-DDEVOURER_MT7612U=ON), 63/63 ctest, make -C src/mt7612u check green including the new beacon TXWI cell, no new warnings — the verification claims hold. I also checked the register work line by line against reference/mt76 @ be5ce79.
Verified against the vendor source
- The
MT_MAC_BSSID_DW1correction is right, bit for bit:mt76x02_regs.h:281-286givesADDR15:0,MBSS_MODE17:16,MBEACON_N20:18,MBSS_LOCAL_BIT21,_B222,_B323.MT_BCN_OFFSET_BASE0x041c,MT_BCN_BYPASS_MASK0x108c,SYNC_MODE18:17 all match too, andinit.cpp:212-216now reproducesmt76x02_mac_setaddr(mt76x02_mac.c:755-760: MBSS_MODE=3, LOCAL_BIT, then MBEACON_N=7) exactly. mt_beacon_init()matchesmt76x02_init_beacon_config(mt76x02_beacon.c:205-213) step for step, and the0xff00 | ~(0xff00 >> 1)unsuppress matchesmt76x02_usb_core.c:223withbeacon_data_count == 1. The slot→bit mapping running down from bit 7 is easy to read backwards from the regs.h comment alone; the beacon.cpp comment does say it.- The APC-slot reasoning now holds by construction.
mt76x02_add_interface(mt76x02_util.c:309-310) derives1 + (((mphy.macaddr[0] ^ addr[0]) >> 2) & 7)aftermt76x02_mac_setaddr, andmt_mac_set_bss_base()is what makes the same invariant true here. The premise correction in the last commit is the right fix.
Blocking
1. src/mt7612u/beacon.cpp:347-350 — the identity retarget still leaks on two failure paths.
const int was_taken = dev->ack_saved;
if (mt7612u_set_ack_responder(dev, ta))
return -1;
if (mt_mac_set_bss_base(dev, ta))
return -1;Both of those bare return -1s skip unwind_identity(). mt7612u_set_ack_responder (caps.cpp:96-110) writes MT_MAC_ADDR_DW0/DW1 and then readback-verifies, so its failure path returns with the port identity already moved — and mt_mac_set_bss_base can only fail after MT_MAC_ADDR has moved successfully. Either way the adapter is left answering for a BSS that was never armed, which is the exact class the f3f08a9/424170b rounds say they closed ("a failed start left it retargeted with no beacon… every failure path unwinds now").
Worse in combination with the new return code: -1 is contracted as "refused before the hardware was touched — whatever was airing still is", so Mt7612uRadio::StartBeacon deliberately leaves _beacon_active alone. A failed re-arm here therefore reports the previous beacon as live while MT_MAC_ADDR now points at the new ta. Both exits need goto fail_pre with took set from !was_taken.
Worth fixing before merge
2. beacon.cpp:433-441 — a failed mt7612u_beacon_update leaves every slot suppressed.
update raises BCN_BYPASS_MASK = 0xffff and nothing lowers it except a successful mt_beacon_write. But mt_beacon_write can refuse on caller-controlled length at :78-83 — which is checked after the guard is up — and can also fail on mt_tx_build or a torn reserved-page copy. Any of those returns with the AP off the air, _beacon_active still true, and no path that re-lowers the guard short of a later successful update. A rejected payload should not silently take the AP down; check the length before raising the guard and unwind the guard on every failure exit.
3. beacon.cpp:426-441 — update accepts any addr2/addr3.
IRadio.h:394-396 says changing addr2/addr3 mid-flight is unsupported and that the port registers keep the StartBeacon identity. beacon_split happily parses a beacon carrying a different BSSID and loads it into the page, so the airing BSSID stops matching the programmed APC slot and MT_MAC_ADDR — beacons perfectly, ACKs nobody, which is precisely the silent failure the start path now goes to real lengths to prevent. Refusing costs a memcmp against what start took.
4. tests/mt7612u_ap_onair.sh cleanup() — the "power-cycle" is an authorized toggle.
The comment says "only a port power-cycle is certain to silence it" and then writes 0/1 to …/authorized. This tree's own CLAUDE.md (Hardware testing) records that as a trap: an authorized toggle is not a cold cycle, it leaves chip state, and VBUS never drops — so an autonomously beaconing MAC keeps beaconing straight through it, which is the one thing this cleanup exists to prevent between cells. Use REGRESS_VBUS_MAP/uhubctl like the rest of the tree, or drop the claim and say what it actually guarantees.
Nits
tests/mt7612u_ap_onair.sh:wait_gone()is defined and never called.regs.h:MT_RX_FILTR_CFG_MCAST/_BCASTare added and unused (the doc's own item-4 conclusion — clearDUPfor an AP so retry evidence stays visible — isn't implemented either).Mt7612uRadio.cppAdjustBeaconTiming/…Fine/PinBeaconTbtt: the log is an improvement, butIRadio.h:452,507already document0as the legitimate "no active beacon" answer, so a programmatic caller is exactly as blind as before. Fine to keep; the comment overclaims what the override changes.docs/mt7612u-ap-mode.md:142-227: "The gap — the driver primitives to add" is now a future-tense description, with LOC estimates and admittedly-unresolvable pre-migration.c:linecitations, of work this PR ships. Items 1, 2, 4, 5 and 6 are done; only item 3 (key install) is still open, and the header already says so. Standing rule here is current-state-only docs — git is the changelog. Fold what's still load-bearing (the MBSS-mask trap, the "address match + beacon is the AP" finding, the measured RX-filter default) into the current-state sections and delete the rest.
One verification gap
The MBSS mask correction changes MT_MAC_BSSID_DW1 at init, on the plain injector/monitor path merged in #422/#423 — from MBSS_MODE=4 (invalid) / MBEACON_N=15 to 3 / 7. Going from invalid to correct is almost certainly an improvement, but it is not AP-scoped, and the verification here is ctest plus the three AP cells. One tests/regress.py pass on the MT7612U before merge would close that.
qodo-gate is failing on both runs, so the merge is blocked on that regardless.
Verdict: changes requested on #1; I'd want #2-#4 in the same round.
josephnef
left a comment
There was a problem hiding this comment.
Second pass, same head (424170b), findings anchored inline this time.
Recap of where the review stands: I built the branch (-DDEVOURER_MT7612U=ON) — 63/63 ctest, make -C src/mt7612u check green including the new beacon TXWI cell, no new warnings, so the PR's verification claims hold. I also checked the register work against reference/mt76 @ be5ce79 and it is a faithful port: the MT_MAC_BSSID_DW1 mask correction matches mt76x02_regs.h:281-286 bit for bit, mt_beacon_init matches mt76x02_init_beacon_config (mt76x02_beacon.c:205-213) step for step, the 0xff00 | ~(0xff00 >> 1) unsuppress matches mt76x02_usb_core.c:223, and init.cpp:212-216 now reproduces mt76x02_mac_setaddr exactly. The APC-slot premise fix is right — mt_mac_set_bss_base() is genuinely what makes the index derivation true rather than lucky on this bench.
One blocking finding (beacon.cpp:349), three worth fixing in the same round, four nits — all inline.
And one verification gap that has no line to attach to: the MBSS mask correction changes MT_MAC_BSSID_DW1 at init, on the plain injector/monitor path merged in #422/#423, from MBSS_MODE=4 (invalid) / MBEACON_N=15 to 3 / 7. Going from invalid to correct is almost certainly an improvement, but it is not AP-scoped and the verification here is ctest plus the three AP cells. One tests/regress.py pass on the MT7612U before merge would close it.
qodo-gate is failing on both runs, so the merge is blocked on that regardless.
Maintainer review at 424170b. The blocking one is mine again, and it is the same class the previous two commits each claimed to have closed. unwind_identity() reached every failure path in beacon_start EXCEPT the two inside the retarget block itself: if (mt7612u_set_ack_responder(dev, ta)) return -1; if (mt_mac_set_bss_base(dev, ta)) return -1; set_ack_responder writes MT_MAC_ADDR and readback-verifies AFTERWARDS, so its failure returns with the identity already moved; set_bss_base is only reachable once that write landed. Both left the adapter answering for a BSS that was never armed. Compounded by the tri-state I added last round: -1 is contracted as "the hardware was never touched, whatever was airing still is", so StartBeacon leaves _beacon_active alone - reporting the previous beacon as live while MT_MAC_ADDR points at the new ta. Ownership is claimed BEFORE those writes now, so the unwind can reach them. Two more on the update path: a payload mt_beacon_write refuses on length left every slot suppressed with nothing to lower them again - the AP silently off the air while _beacon_active still said otherwise. The length rule is factored out and applied before the guard goes up, and every failure exit lowers it. update accepted a beacon carrying a different addr2. IRadio says the port registers keep the StartBeacon identity, so that airs a beacon matching neither the APC slot nor MT_MAC_ADDR - beacons perfectly, acknowledges nobody. It remembers what start programmed and refuses. The `authorized` toggle: measured rather than argued. CLAUDE.md is right that it is not a cold cycle - VBUS never drops - and the comment claiming "power-cycle" was wrong. But the specific concern, that an autonomously beaconing MAC rides through it, does not hold for this part: armed -> SSID seen host process killed -> SSID STILL seen (the beacon is autonomous) authorized toggle -> SSID gone So the comment now says what it is measured to guarantee, and AP_VBUS= <hubloc>:<port> gives a real cold cycle through uhubctl for anyone who wants one - hub ports only, never an xhci root port, which is where both adapters on this bench sit. Nits: implemented the AP RX-filter DUP clear the doc concluded and the regs.h defines were added for - dropping duplicates hides a station's retransmission, which is the retry evidence that says whether the ACKs are landing. Removed the dead wait_gone(). The beacon-steer trio's comment claimed more than the override delivers: it still returns 0, which IRadio documents as the "no active beacon" answer, so a programmatic caller is no better off and only an operator reading a log is. Rewrote "The gap - the driver primitives to add" as current state; five of its six items ship in this PR and git is the changelog. Verification gap closed: the MBSS mask correction changes MT_MAC_BSSID_DW1 at init on the plain injector path from OpenIPC#422/OpenIPC#423, not just the AP path, so tests/regress.py on two MT7612U - 4/4 cells green, 9100/9231 hits devourer-TX both directions. AP harness after all of it: 14 passed, 0 failed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Tba83kymS5W2v1vn2yRxrj
|
Thanks — that's a thorough one, and the blocker is a fair hit. Pushed The blocker. You're right, and it stings a bit: I added The tri-state interaction you spotted is the worse half. #2 and #3 fixed as described: the length rule is applied before the guard goes up and every failure exit lowers it; #4, the Re-enumeration stops the timer. CLAUDE.md is still right that it is not a cold cycle — VBUS never drops, chip state survives — so the comment now claims only what that sequence shows. I did not make uhubctl the default because both adapters here are on xhci root ports, which this rig has previously wedged to the point of needing a machine power-off; Nits: Your verification gap, closed. You were right that this wasn't AP-scoped — the MBSS mask correction changes
AP harness after everything: 14 passed, 0 failed. One open question back to you, unchanged from the PR body: hardware CCMP is the single remaining gap and it is reachable in ~40 lines over primitives already here — but |
josephnef
left a comment
There was a problem hiding this comment.
Re-reviewed at c5c0c7e. Rebuilt: 63/63 ctest, make -C src/mt7612u check green, no new warnings.
Findings 2, 3 and the nits are all properly closed — len_fits_slot() factored ahead of the guard with an unwind on the write failure, beacon_ident refusing an addr2 change, wait_gone() gone, the steer-trio comment trimmed to what it delivers, and "The gap" rewritten as current state. Thank you for the regress.py run on the injector path; 4/4 green closes the verification gap I flagged, and that was the answer I wanted rather than an argument.
On #4: your measurement beats my objection, and I'll take it. authorized → SSID gone is the thing that matters here, and the comment now claims only that. AP_VBUS for a switchable hub port is the right shape — no, don't make it mandatory; your bench's root ports are exactly the case that would make the script unrunnable for the person most likely to run it.
On #1: the fix moved the bug rather than removing it. mt7612u_set_ack_responder() clears beacon_took_identity itself, so claiming ownership before calling it means the claim is wiped on the success path and StopBeacon no longer restores the identity. Detail inline at :365. Failure paths are correct now — it is the success path that regressed.
One new one at :418 on the DUP clear, which I think is the right change made in the wrong place: it is never restored, including on a fail_pre exit that is contracted as "hardware never touched".
On hardware CCMP — open it as its own issue, and don't add a key surface to IRadio for it. A method most backends answer with a not-ported default is the pattern this tree already uses, but a crypto key interface is a much bigger contract than FastRetune (key lifetime, GTK vs PTK, rekey, who owns the replay counter), and getting it wrong is expensive to undo once callers exist. It also isn't blocking anything: the software CCMP path is shipped, measured here, and shared with the Realtek backends. Land the beacon plane, then design the key surface against two backends rather than one.
Everything else stands; happy to approve once :365 and :418 are closed.
Five fixes. Two of them are defects this branch introduced - one last round, one in the first draft of this one. 1. mt7612u_clear_ack_responder() keeps ack_saved when its own writes fail. unwind_identity()'s comment claimed the flags survive a failed restore "so Stop()'s documented retry has something left to retry". True for the MBSS half; false for MT_MAC_ADDR. clear_ack_responder() cleared ack_saved unconditionally and early-returns on !ack_saved, so attempts two and three were no-ops against a still-leaked port MAC - the retry the comment promised could not reach the register it was written for. It now keeps the flag on an mt_io_errors() delta, same idiom as the callers. Both halves of the retry have to agree before either works. 2. mt7612u_beacon_update() compares addr3 as well as addr2. IRadio.h:394-396 names both; only addr2 was checked. addr3 is the half programmed into the APC slot, so an update that moved only addr3 aired a BSSID the slot does not hold - the deaf AP the guard exists to prevent, through the guard. beacon_ident is 12 bytes now and one memcpy covers the adjacent pair; beacon_split() already requires a 24-byte header, so the range is in bounds by construction. 3. set_ack_responder() keeps its MT_AUTO_RSP_EN re-assert out of the I/O accumulator. Introduced by fix 1's sibling change - moving beacon_start's io_err snapshot above the identity writes, which was right for MT_MAC_ADDR_DW1 and wrong for this. mac_reset() already sets MT_AUTO_RSP_EN (init.cpp:174), so the mt_set() here is a documented no-op; but mt_set() is mt_rmw(), which on a failed READ bumps io_err and skips its write, and its return is ignored because the readback below is the real check. One transient EP0 read stall on MT_AUTO_RSP_CFG therefore tore down a verified-good arm: -2 from beacon_start, _beacon_active cleared, for a beacon that was on the air. 4. -1 no longer escapes from inside the identity block. The tri-state contracts -1 as "nothing was touched, whatever was airing still is", but the only two paths returning it were reachable *after* set_ack_responder had written MT_MAC_ADDR. The unwind restores the FACTORY address - the only one saved anywhere - so a failed re-arm over a live beacon retracted an identity that beacon never had while its page and timers kept airing it, and the caller was told nothing changed. There is no atomic re-arm to offer: one MT_MAC_ADDR, one save slot, and the previous occupant is not in it. Those exits go to fail_post now, so -1 belongs to the input refusals above the first hardware write and means what it says. 5. The RX-filter comment attributes DUP-clear correctly. It said "this backend's receive default is already what an AP wants". The default is 0x00015f97 (initvals.h, init.cpp:290) and MT_RX_FILTR_CFG_DUP is SET in it. mt7612u_set_monitor_rx() is what leaves it clear, deliberately, and every AP path reaches it via StartRxLoop; a TX-only InitWrite does not and has no receiver to count retries with. Also drops MT_RX_FILTR_CFG_MCAST and _BCAST, which this branch added and nothing uses. Tested: 63/63 ctest, subtree check green, and tests/mt7612u_ap_onair.sh 14/14 on ch36 and 14/14 on ch6. The update guard has a positive control plus two negative arms against a live beacon in mt7612u_beacon_stop_check.cpp - an unchanged payload must be accepted, so a guard that refused everything would not pass. Only the addr3 arm discriminates against the pre-fix code; the addr2 arm is a regression check and the test says so. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Tba83kymS5W2v1vn2yRxrj
|
@josephnef pushed
Three you have not seen, because they came out of an adversarial pass over my own diff rather than from the bench:
Every one of those leaves the beacon airing perfectly, which is why the harness could not find them.
Docs: the future-tense section is gone. Three findings survive it as current-state prose — the address-match-is-the-AP property, the MBSS/APC index trap, and the measured RX-filter default. Also corrected: the harness takes Hardware CCMP is #425, deliberately without an Verification on I resolved the twelve qodo threads that are fixed and left yours for you. |
josephnef
left a comment
There was a problem hiding this comment.
Verified at 6465ab1 — rebuilt, 63/63 ctest, subtree check green, no new warnings. Both my threads are closed, and closed properly rather than papered over. Resolving them; approving.
:365. Right shape now: took decided before the writes so the in-flight failures know we own the identity, dev->beacon_took_identity set after set_ack_responder so its ownership-transfer write at caps.cpp:90 can't clobber the claim. I checked the success path is the one that actually changed — that was the regression.
Dropping the !ack_saved guard is the better call and your reasoning for it is the part I'd have missed: ack_saved is set by this function's own call, so a re-arm over a live beacon took the "somebody else got here first" branch and disabled the restore for the session — while the retarget had already overwritten the address the branch was nominally protecting. A guard that fires on its own side effect and protects nothing when it does is worse than no guard.
:418. Removing it beats the save/restore I asked for, and for a reason I had backwards: mt7612u_set_monitor_rx() rewrites MT_RX_FILTR_CFG to PHY_ERR (+CRC_ERR) wholesale, so DUP is already clear on every path that beacons — Mt7612uRadio::StartRxLoop():407, and an AP has to receive. Restoring it at stop would have switched duplicate filtering on in a session that had it off and destroyed the retry=0 evidence. My finding was right about the residue and wrong about the fix; good catch not taking it at face value.
The three you found yourselves are the interesting ones, and all three are the same shape — the beacon airs perfectly and the harness cannot see the defect:
- The
io_errbracket overmt_set(MT_AUTO_RSP_EN)is the sharpest.mt_rmwbumping the counter on a failed read of a bit the code documents as already-set, tearing down a verified-good arm and returning-2for a beacon that is on the air — that is a false negative manufactured by a fix for a false positive.mt_io_restore()around it is right, and it is the existing pattern (fw.cpp:299), not a new mechanism. -1escaping from inside the identity block: agreed, and deletingfail_preis the right resolution rather than trying to make the promise true. I checked the flow — every-1inbeacon_startis now above themt_io_errors()snapshot at:388, and everything past it goes tofail_post. The tri-state means what it says now.- addr3: fair. I wrote "addr2/addr3" and only read back the addr2 half of what shipped.
The test. Saying which arm discriminates, and keeping a positive control against a guard that could pass both negatives by refusing everything, is the right instinct — a test whose arms aren't told apart reads as twice the coverage it has. Same for the CH= vs DEVOURER_CHANNEL= correction; a documented invocation that runs ch36 with a 5180 MHz scan is the kind of thing that costs someone an afternoon.
Verification. ch36 and ch6, 14/14 each, carrying association, data plane, WPA2 and auth … retry=0 — that is a stronger claim than the single-band table the PR body started with, and it is the right one to have made before merge. regress.py closed the injector-path question. All checks green including qodo-gate.
Ship it. #425 is the right home for hardware CCMP, and naming software CCMP's unmeasured per-frame cost in it is what keeps "hardware crypto is a gain here" honest until someone measures it.
Follows #419's integration (#420-#423). That gave devourer an MT7612U it can
open, receive and transmit on. This gives it one it can be an access point
on — open and WPA2-PSK — with no MediaTek-specific code in the AP layer.
It works, and devourer's own harnesses are what prove it
tests/ap_responder.cppandtests/ap_wpa2.cppagainst an MT7612U, with nochange to their AP logic. Both already took an
IRadio*— #415 made themgenuinely backend-agnostic — so nothing in them knows this is MediaTek. (They
each gained a dozen lines that silence the beacon before
_exit; see below.Nothing in that is MediaTek-specific either.)
retry=0on the auth is the load-bearing one: an un-ACKed frame isretransmitted with FC Retry set, so retry=0 is the hardware ACK, and it is
the only evidence that the port identity and the APC slot are both right.
That output is
tests/mt7612u_ap_onair.sh, added here, so the numbers are acommand rather than a story. 14/14 on ch36 and 14/14 on ch6 — both bands
carry association, the data plane, WPA2 and the auto-ACK, not just a beacon.
What was actually missing
Nothing in the AP layer, and no new driver primitives.
StartBeacon,UpdateBeaconPayloadandStopBeaconfell through toIRadio's base-classreturn false, so a MediaTek adapter beaconed nothing. Everything underneathwas already device-verified (
mt_beacon_init/_write/_set_enable,mt_ap_set_bssid), andSetAckResponder,ReadTsfandWriteTsfwerealready implemented.
Three public entry points, because the backend reaches the subtree through the
public header and
api_linkholds that line (31 now):The identity is the whole trick.
IRadiosays addr2/addr3 set the portMAC/BSSID,
RtlJaguarDeviceimplements exactly that, andap_responder's owncomment says "MACID = BSSID, set by StartBeacon". My first draft instead
refused a BSSID that was not the adapter's own MAC — which would have made
both shipped harnesses unusable on this part, since both hardcode
02:42:75:05:d6:00. It now followsmt76x02_mac_setaddr— moveMT_MAC_ADDRand the
MT_MAC_BSSIDbase together, then derive the APC slot the waymt76x02_util.c:310does. Getting that wrong is the review finding below,and it is the reason the two registers move together rather than one of them.
Also fixed, not MediaTek-specific
ap_responder,ap_wpa2andul_trigger_apall arm a hardware beacon andthen
_exit(0), skipping every destructor — soStopBeaconnever ran and thebeacon kept airing after the process was gone, until the adapter was
power-cycled. Measured: a scan after exit found the SSID live at
last seen: 308 ms ago. The beacon is hardware-autonomous on the Realtek parts too, andIRadio.hhas warned about this since it was written;beacon_update_probe.cppalready called StopBeacon before its own
_exit, these three did not.Twelve lines each, not one:
StopBeaconbecame able to fail during thisreview (it could not before — see below), and #411 amended
IRadio.hthe sameday to say such a failure "must be retried (or followed by hardware shutdown)
before its shared port is reused". So each harness retries up to three times
and warns if the beacon is still armed, rather than calling it once and
discarding the result.
What review changed
Five rounds, two of which returned DO-NOT-MERGE and were right to. The
headline finding is still the one worth reading:
The APC slot index rested on a premise that is false in this codebase. I
derived it as
(ta[0] & 2) ? 1 : 0, arguing mt76's1 + (((macaddr[0] ^ addr[0]) >> 2) & 7)collapses because the identity hadjust been retargeted. It does not.
dev->macaddris written once, from theEEPROM, and
MT_MAC_BSSID— the register carryingMBSS_MODE, and the basethe hardware derives the index from — is written once at init from the factory
MAC; the ACK-responder retarget moves only
MT_MAC_ADDR. So the hardware wasindexing off a different address than the host assumed, and the constant was
correct only where
1 + (((factory[0] ^ ta[0]) >> 2) & 7)happens to be 1.This bench is exactly such a case (
40:a5:ef:…, so the XOR term is 0), whichmeans every on-air run agreed with the wrong reasoning. On an adapter whose
first byte is
0xe8the AP would beacon perfectly and acknowledge nobody. Thebring-up gate had refused that case; the ABI flattened it to a constant. Fixed
by making the retarget a real
setaddr, so the premise is true by construction.Second:
mt7612u_beacon_stopcould not fail —mt_clearreports only itsread half and the
mt_ap_set_bssidreturns were dropped — which madeStopBeacon's failure branch unreachable and the new harness's assertion forit vacuous, while the real hazard (an EP0 stall leaving the MAC beaconing)
reported success. Also fixed: the reserved-page copy could fail mid-frame and
report success (a torn beacon airs, worse than none);
UpdateBeaconPayloaddid not raise the suppression guard mt76 explicitly brackets for that reason;
Stop()discarded the result while promising a retry that did not exist; andthe public header still described the pre-fix behaviour.
The earlier round found eight more, one reproduced with a probe against the
real object file:
beacon_splitleft four of ninemt7612u_tx_ratefieldsindeterminate, and
sgi/ldpc/stbcgo straight into the rate word the MACtransmits verbatim while
power_adjshort-circuits the derived TX power.The last two rounds were about fixes that moved the bug rather than removing
it, which is worth calling out because it happened three times:
block that moves the identity were the ones not routed through it.
MT_MAC_ADDRwas claimed before the call that transfersownership away, so the claim was immediately clobbered and the success
path stopped restoring.
MT_MAC_ADDRat all:mt7612u_clear_ack_respondercleared its own "restore owed" flagunconditionally and early-returns on it, so attempts two and three were
no-ops against a still-leaked port MAC. Both halves of the retry had to agree
before either worked.
beacon_start'sio_errbracket over amt_set(MT_AUTO_RSP_EN)the code itself documents as a no-op (the gate isalready on from
mac_reset).mt_setismt_rmw, which bumpsio_errandskips its write when its read half stalls, and its return is ignored
because the readback below is the real check — so one transient EP0 read
stall tore down a verified-good arm and returned
-2for a beacon that wason the air.
The last of those was found by an adversarial pass over the diff, not by the
harness: every one of these failures leaves the beacon airing perfectly, so no
amount of watching the air finds them.
Relatedly,
-1no longer escapes from inside the identity block. It iscontracted as "nothing was touched, whatever was airing still is", and the two
exits returning it were reachable only after
MT_MAC_ADDRhad moved — andthe unwind restores the factory address, the only one saved anywhere. A
failed re-arm over a live beacon therefore retracted an identity that beacon
never had while its page and timers kept airing it, under a return value
promising nothing changed. There is no atomic re-arm to offer (one
MT_MAC_ADDR, one save slot, previous occupant not in it), so those exits takethe AP down deliberately and return
-2.-1now belongs to the inputrefusals above the first hardware write.
Rebase notes
The AP work predates the C++ migration (#421) and the backend (#422). Rebasing
surfaced that
beacon.cwas compiled by nothing — the subtree Makefileglobs
*.cppand the CMake list is explicit — plus C11_Atomic, implicitvoid*conversions, and amemsetover a no-longer-trivially-copyable type.The original four AP commits and that repair are squashed into one, because
the first three do not build on today's master and a bisect through them
returns build failures rather than the commit you are looking for.
What this does not show
In
docs/mt7612u-ap-mode.mdat the same length as the numbers:CCMP, the same code the Realtek backends use.
MT_WCID_KEYis not evendefined in this tree and the key path is unreached, so "crypto becomes
hardware on MediaTek" remains a claim. It is more than a flag:
MT_WCID_KEYand
MT_WCID_IVare both absent,mt_tx_build()setsMT_TXD_INFO_WIVunconditionally (meaning "not encrypted") where mt76 gates it on the WCID
having a key, and RX would have to strip the IV/PN itself. Opened as its own
issue rather than guessed at here, and deliberately not proposing an
IRadiokey surface — per your guidance, that contract should be designedagainst two backends rather than one.
Protected bit; "encrypted" rests on the verified MIC plus traffic reaching a
CCMP-only station.
mt76x2u), so it is not anindependent-generation witness. The RTL8812AU witness in the earlier Stage
A/B section is.
station, no rekey, no roaming, no channel change while beaconing.
Verification
ctestwithDEVOURER_MT7612U=ON, and with it OFF;make -C src/mt7612u checkgreen (4 binaries,api_linkat 31 entry points); nowarnings in either build.
original series did not (the first three predate the C++ migration and their
repair came fourth; they are squashed into one).
tests/mt7612u_ap_onair.sh14/14 on ch36 and14/14 on ch6.
tests/regress.py4/4 on the injector path — devourer↔devourer,devourer↔kernel both directions — so the AP changes did not disturb the
transmit/receive behaviour MT7612U integration: wire src/mt7612u in behind IRadio #419 shipped.
ones, run against a live beacon: an unchanged payload must be accepted, so
a guard that refused everything would not pass. Only the addr3 arm
discriminates against the pre-fix code — addr2 was already guarded — and the
test says which is which rather than counting both as new coverage.
🤖 Generated with Claude Code
https://claude.ai/code/session_01Tba83kymS5W2v1vn2yRxrj