Repository navigation
sta: station client on every station-capable die, AP restart survival, MT7612U managed RX filter - #471
sta: station client on every station-capable die, AP restart survival, MT7612U managed RX filter#471snokvist wants to merge 53 commits into
Conversation
A station cannot see the AP's side of an association. An Association Response the station received but whose acknowledgement the AP never saw leaves the AP without the station while StationSm sits Connected, and on an open BSS nothing in the protocol ever says so (seen on air with an 8822C station against an mt76 AP: the AP's status for the response stayed pending, no deauthentication followed). What proves the AP's side is a unicast reply to the station's own traffic, which only the data plane sees, so the check belongs to the caller. link_lost() is its way back into the ordinary failure path: from Connected it fails the association with the new Failure::Unconfirmed (no AID, queue cleared, as any failure); anywhere else it changes nothing. on_assoc_resp also counts Association Responses that arrive when none is awaited (rx_assoc_repeat) instead of dropping them silently - an AP retransmitting one is the visible half of a lost acknowledgement. station_sm: test_link_lost. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VNC8xhn1rNCi5t6uLvE6M3
SetStationIdentity now installs MT_RX_FILTR_CFG_MANAGED, the managed filter every station cell measured, in place of the monitor filter the RX loop installs, and reads it back before the arm counts. ClearStationIdentity puts the replaced value back and returns true only once it reads back; a failed clear keeps the arm recorded so a second clear retries. While armed, mt7612u_set_monitor_rx() records its request instead of installing it, so the arm is order-independent against StartRxLoop. A port-identity drop (beacon / ACK responder) restores the pre-arm filter and a restore re-installs the managed one; both writes are read back and a miss is WARNed (kept out of the caller's I/O-error accumulator). Refusals write nothing. Stop() clears a still-armed station before closing, best effort - the chip keeps the managed filter across a close otherwise - and logs a failure only after the stop and close. mt7612u_clear_station_identity() now returns int (0 restored or nothing armed, -1 not verified). IRadio.h / AdapterCaps.h change comments only. The policy is pure (StationIdentity.h) and covered by ctest mt7612u_station_identity, including the bit decode of 0x00015f97 (regs.h names the bits); the mt7612uprobe staid gate checks the filter across arm / re-request / refusal / clear / drop. Addresses the managed-filter item of OpenIPC#461. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VNC8xhn1rNCi5t6uLvE6M3
tests/realtek_station_onair.sh carried its own rt_record / rt_opened /
rt_handback for adapters devourer opens over libusb, and the lib carried
the same three for PEER_SYSFS only. They are now one set in
tests/mt7612u_sta_lib.sh, kept per NAME in files in OUT (POSIX, and a
process started in a command substitution can still mark a device
opened):
sta_dev_record NAME SYSFS VID PID refuse a hub or the wrong VID:PID,
note idVendor:idProduct:serial
sta_dev_opened NAME just before a process opens it
sta_dev_handback NAME SYSFS authorized 0/1 toggle, only when this
run opened it and SYSFS still names
the recorded device; idempotent
sta_peer_record / _opened / _handback stay as thin wrappers, so the
mt7612u_sta_* harnesses are unchanged. sta_dev_unbind_wifi SYSFS unbinds
the kernel driver from every interface of a device that carries a
wireless netdev (rtw88, an out-of-tree rtl88x2*, mt76x2u; not a composite
adapter's Bluetooth interface) and refuses if one is left bound.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VNC8xhn1rNCi5t6uLvE6M3
…indows
tests/mt7612u_sta_onair.sh becomes tests/sta_client_onair.sh: the DUT is
an MT7612U, an RTL8812CU (8822C) or an RTL8812BU (8822B) - the dies whose
station_mode_ok is true - and the AP any netns-movable in-kernel adapter,
e.g. the MT7612U on mt76x2u.
- DUT take / hand-back by die: the MT7612U keeps sta_dut_take; a Realtek
DUT goes through the lib's sta_dev_record / sta_dev_opened /
sta_dev_unbind_wifi / sta_dev_handback, with the same rule - never
re-enumerated while sta_client is alive. sta_client gets the die by
DEVOURER_VID / DEVOURER_PID from the DUT's sysfs identity.
- noarm is a real control on Realtek, where the arm writes the port
registers and unarmed the MAC does not ACK own-addressed unicast: scored
that the station tried (beacons seen, authentication sent) and the AP did
NOT complete the four-way; a completed one FAILs. On MT7612U the arm
writes nothing and the link stays information.
- The clear is scored as verified on Realtek, information on MT7612U.
- reconnect: hostapd stopped for DOWN_S and restarted; scored the lost
link reported, a second four-way within REJOIN_S, ping 0% over a PING_S
window, ledger 2 associations / 1 reconnect, exactly one arm across the
re-join (the arm is per BSSID and stays in place), the clear.
noreconnect: the same with DEVOURER_STA_RECONNECT=0 - lost link
reported, no re-join, ledger ends Failed after 1 association.
- noarm and retry0 report their link over a PING_S ping window (2/s)
instead of six pings.
sta_client logs each association ("station connected (association N)")
and each failure ("station link lost: <reason>" / "station join failed:
<reason>"), so a re-join is visible while the run lasts.
docs/station-client.md documents the cells, the per-die arm scoring and
the re-join policy, including DEVOURER_STA_RECONNECT=0.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VNC8xhn1rNCi5t6uLvE6M3
The ledger's first line read the state machine after the teardown's leave(), which always returns it to Idle, so every run - including one that gave up with DEVOURER_STA_RECONNECT=0 - reported state=Idle with no reason. The end state (state, failure reason and status, AID, keyed) is now taken under g_mu just before leave(), and report() prints that: Connected for a run that ended associated, Failed reason=<why> for one that gave up. test_reconnect_can_be_disabled now also checks that leave() returns to Idle while the recorded end state is Failed, reason beacon-lost. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VNC8xhn1rNCi5t6uLvE6M3
- The lib clears .id_* / .opened_* when a run takes OUT's lock: a reused OUT no longer carries an earlier run's device records into this run's hand-back (realtek_station_onair.sh HALF=up never records `peer`). - reconnect measures its re-join bound, and the printed time, from the moment hostapd is started again, not from ap_up returning. - The Realtek noarm control has a positive control: unless the ARMED wpa2 cell of the same run completed a four-way against the same hostapd configuration, a silent AP proves nothing and noarm is INCONCLUSIVE. - wpa2 scores the two MIC counters for what they are: the four-way's (mic_failures=) must be 0, the data plane's (MIC failures=) may reach one per pairwise rekey. - AP_OFDM_ONLY=1 (2.4 GHz) makes hostapd advertise and use OFDM rates only, so its authentication and association responses go out at 6 Mb/s instead of 1 Mb/s CCK - the first experiment on the 8822C association issue, which docs/station-client.md now records as a known issue. - noreconnect's "ends Failed" check now holds against the client's end state; the doc says the ledger reports the state the run ended in. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VNC8xhn1rNCi5t6uLvE6M3
…AP never held On air an 8822C station intermittently received its Association Response while the AP never saw it acknowledged: the AP never added the station, dropped its traffic and sent no deauthentication, and on an open BSS the station sat "connected" until the run ended. (On WPA2 the four-way timeout already recovers it.) An open association now counts as unconfirmed until a unicast data frame from the AP arrives for the station. Once the host has asked something - kConfirmUplink (3) unicast or ARP frames - and kConfirmMs (5 s) has passed with no such reply, supervise() calls StationSm::link_lost(): the link is lost as "unconfirmed" and the ordinary re-join policy (backoff, DEVOURER_STA_RECONNECT) takes over. Multicast chatter and an idle host are never judged. The ledger gains unconfirmed= and assoc_repeat=. Headless: an unconfirmed association is lost and re-joined; a unicast reply confirms for good; an idle host and multicast chatter are never judged; unanswered ARP requests are. tests/sta_client_onair.sh's open cell now runs a ping from the moment the TAP is up, as a user's host would, and scores hostapd's AP-STA-CONNECTED for our address within 30 s - covering one recovered association - before the scored ping; a recovery is reported. docs/station-client.md documents the rule and restates the 8822C known issue as intermittent, not CCK-only. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VNC8xhn1rNCi5t6uLvE6M3
…ed cell HOSTAPD_DEBUG=1 runs hostapd with -dd into the cell's hostapd log, for the AP's view of an association (station add, TX status). Off by default; meant for the open cell, since the extra lines can repeat the text the wpa2 cell counts for its rekeys. A cell that FAILs now saves the tail of dmesg to dmesg_<cell>.txt and echoes its mt76 / rtw88 / cfg80211 lines - read-only, best effort - so an AP driver error during station add shows next to the verdict. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VNC8xhn1rNCi5t6uLvE6M3
… WPA2
An MT7612U AP (kernel mt76x2u) can hold a transmitted frame's TX status
until its next transmission - the same silicon behaviour
docs/mt7612u-tx-retry.md records for this tree's own MT7612U driver.
hostapd counts a station associated, and starts the WPA2 four-way, only
once the Association Response's status is in. hostapd's debug log of a
failing run: "association OK", the response sent, no TX status for six
seconds, then - when the station's next frame made the AP transmit - the
status with ack=1, too late ("handle_assoc_cb: STA ... not found"). On WPA2
every attempt reads "authenticated", then nothing until the station's
handshake timeout. The station had acknowledged the response; it was not
at fault.
The moment an Association Response is accepted - Associating to Connected
(open) or FourWay (WPA2) - sta_client queues one probe request (every AP
answers one), so the AP transmits and a held status is released. On WPA2 a
second one follows if no EAPOL has arrived kNudgeAgainMs (1 s) later, and
none after that. The safety nets stay: the confirmation rule (open) and the
four-way timeout (WPA2). The ledger counts the nudges.
Headless: open is nudged once and never again; WPA2 is nudged on the
accepted response, once more after 1 s without EAPOL, never a third time,
and not a second time when message 1 arrives at once. A nudge is exactly
one probe request: it does not retune, does not open the retune guard
(g_tuned / g_retune_ms) and does not move the scan sweep; the probe
response it provokes counts as AP liveness and leaves the BSS on its
channel.
docs/station-client.md: the nudge, and the AP quirk.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VNC8xhn1rNCi5t6uLvE6M3
Plaintext data from our BSS on a WPA2 link was refused silently. It is now counted (`plaintext refused` in the ledger's data-plane line), leaving out the four-way's own cleartext EAPOL (recognised by the full SNAP header, is_ethertype_snap + 0x888e) and the no-data subtypes (Null / QoS Null), which carry nothing to refuse. The on-air harness uses the count as its positive witness that unicast addressed to the station gets through the MT7612U's managed receive filter. The header comments say what the RX path now is on MT7612U: promiscuous until the arm, the managed filter while armed, so StationSm::on_rx's `not-for-us` count is that filter's witness. Headless: an unprotected data frame on a protected link is counted as refused; a 26-byte QoS Null is not. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VNC8xhn1rNCi5t6uLvE6M3
With an MT7612U DUT, once the wpa2 / noarm four-way is in, a monitor vif on the AP's phy injects two plaintext unicast data streams from the AP's BSSID (INJECT_S at INJECT_PPS each): one to FOREIGN, an address nobody holds, and one to the station's own address. The own stream is the positive witness that the injection reaches the DUT: the station refuses it on a WPA2 link and counts it (`plaintext refused`), and it must reach half of what was injected, else the check is INCONCLUSIVE. Then armed (wpa2) `not-for-us` must stay under 1% of the foreign stream; unarmed (noarm, the monitor filter) at least half of it must arrive. A phy that cannot add a monitor vif makes the filter check INCONCLUSIVE, not the cell. On a Realtek DUT the injection is skipped and said as INFO. The two streams share a transmitter address, so the own stream starts at sequence 2048 (sta_unicast_inject.py gains an optional seq0, default 0; existing callers unchanged). Each injector is bounded by `timeout -k`, its PID recorded, and killed - and the monitor vif deleted - by the cleanup on every exit path, before the AP phy leaves the namespace. The clear is now scored as verified on both dies: on MT7612U it restores the monitor filter and reads it back, so it is no longer trivially true. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VNC8xhn1rNCi5t6uLvE6M3
- StationIdentity.h: an arm after a DROP keeps the recorded pre-arm filter instead of recording the register. The drop's restore write may have missed and left the managed filter there; recording it made the clear "restore" a managed receiver and report it verified. Headless check in test_rx_filter_ownership. - Mt7612uRadio::Stop(): the best-effort clear is guarded. Its own WARN goes through the same logger as everything else (log_trampoline), and an escape there - with `_dev` already nulled - skipped the stop and the close and leaked the handle, which is exactly what the comment said must not happen. - mt7612uprobe staid: the two clears between cases are checked (the second one is the lost-arm clear that re-writes and verifies the filter) instead of their results being dropped. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VNC8xhn1rNCi5t6uLvE6M3
…stion
The open-association confirmation judged "kConfirmUplink frames sent and
kConfirmMs since the ASSOCIATION": a host idle for longer than kConfirmMs
that then sent a burst of three unicast frames (a neighbour still cached
from a previous association needs no ARP first) was judged lost on the
next loop pass, before any reply could arrive, and re-joined a healthy AP.
The window now opens the first time supervise() sees a question, which is
what docs/station-client.md already said ("within 5 s"). Headless:
test_a_burst_after_idle_gets_its_window (fails against the old rule); the
existing cells open the window explicitly. The comment and the doc now
state the one-way-unicast limit as it is: judged until the host's stack
re-verifies the neighbour, not only with static entries.
on_association() goes back to taking no argument.
sta_client_onair.sh: a timed-out AP-netdev wait leaves the interface up,
as the same block in OpenIPC#466 does, so the two read the same.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VNC8xhn1rNCi5t6uLvE6M3
When the managed filter did not read back, the arm put the previous value back with a bare unchecked write. If that missed too, the register was left managed with nothing recorded: ClearStationIdentity reported "restored (verified)" for a receiver still managed, and a retried arm (sta_client retries) read the managed value back as the pre-arm one. The undo is now verified. An undo that misses is recorded (mt7612u_sta_strand: `stranded`, with the pre-arm value), so the clear still restores it, a filter request is still recorded, and the next arm keeps the recorded value instead of reading the register. Headless: test_stranded_undo_keeps_the_pre_arm_filter. docs/mt7612u-station-identity.md: says so, and no longer claims the RX loop never writes the filter - StartRxLoop's mt7612u_set_monitor_rx() does, mediated while armed. The Stop() comment no longer overstates what the catch protects (mt7612u_stop sits outside it). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VNC8xhn1rNCi5t6uLvE6M3
…ixes sta_client: the open-association confirmation counted any host unicast as a question, so a one-way UDP uplink to a peer behind the AP (video, telemetry - the FPV case) got nothing unicast back and was judged lost and re-joined every few seconds, for as long as it ran. Only a frame whose answer the AP must forward back now counts (solicits_reply): an ARP request that is neither gratuitous nor a DAD probe, an ICMP / ICMPv6 echo request, a unicast IPv6 neighbour solicitation, TCP, a DNS query. Headless: test_a_one_way_udp_uplink_is_not_judged (and DNS still is), gratuitous and probe ARPs not counted; the confirmation fixtures send real ICMP / TCP / ARP requests. Both fail against the old predicate. The comment and docs/station-client.md say what an unheld association under one-way traffic costs instead. probe() returns whether it built a frame, and a nudge is counted only then. sta_client_onair.sh: - the open cell's background ping is killed on the station-gone path too; - an armed managed-filter PASS is held until the noarm control of the same run has seen the foreign stream arrive (the own stream witnesses the injection path, not the foreign injector), else INCONCLUSIVE. mt7612u_sta_lib.sh: the one-run-per-OUT lock is an flock(1) on the OUT directory. mkdir plus a separate PID write let a concurrent run read an empty PID, reclaim the live lock and share OUT. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VNC8xhn1rNCi5t6uLvE6M3
An overrun BSSID gate sets r_bss=2 and the exit code calls the run INCONCLUSIVE, but the verdict `case` had only 0, 3 and *, so the operator read "the BSSID gate did not pass". The probe-response gate had the same gap: arm C never reported is r_ack=2, printed as "arm C did not hold". Both now have a 2) branch that says INCONCLUSIVE. Refs OpenIPC#467. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VNC8xhn1rNCi5t6uLvE6M3
On a host whose MediaTek firmware is zstd-compressed (only /lib/firmware/mediatek/*.bin.zst), sta_fw_link succeeded, the probe then logged "cannot open firmware/mt7662_rom_patch.bin", and the uplink harness scored "ABORTED the DUT did not confirm retry limit 15:" with an empty reason (identity: "could not read the DUT's MAC", exit 1). sta_fw_readable checks both blobs are present, readable and non-empty, and names the .zst case when that is what it finds. sta_fw_link runs it through the link it made (or found), so identity, autoack and uplink refuse the rig (exit 2) in seconds, before the DUT is taken. sta_client_onair runs it on FW_DIR for an MT7612U DUT. Refs OpenIPC#467. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VNC8xhn1rNCi5t6uLvE6M3
realtek_station_onair, sta_client_onair and mt7612u_sta_identity already exited 3 on an interrupt; mt7612u_sta_autoack, mt7612u_sta_uplink and mt7612u_ap_onair still exited 130. All six now follow the one convention: 0 pass, 1 fail, 2 inconclusive, 3 interrupted. The autoack and AP harness headers now state it; no caller in the tree reads 130. Refs OpenIPC#467. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VNC8xhn1rNCi5t6uLvE6M3
On the KILL path reap cleared KIDS without `wait`ing anything, so every child of that cleanup - the KILLed one and the ones TERM had already ended - stayed a zombie for the rest of the run. reap now gives a KILL 2 s to land (it waits for the process to leave the kernel), then waits every child that has exited, on both paths. A child still running after that is not waited on, since that wait could block. The return value is unchanged: 1 when anything had to be KILLed. Checked with a TERM-ignoring child beside two plain ones: rc 1, no zombies left; a clean reap still returns 0. Refs OpenIPC#467. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VNC8xhn1rNCi5t6uLvE6M3
The final tx.stats t can precede the last tx.report t by a few ms (-6 ms in one review arm), and summarize printed that as a negative silence. It was harmless to `live`, which only compares tail_ms against MAX_GAP_MS, but it read as a clock fault. tail_ms is now clamped at 0, and the summarize comment says why. Checked on a fixture whose final tx.stats is 6 ms before the last report: tail_ms=0, live=1. Refs OpenIPC#467. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VNC8xhn1rNCi5t6uLvE6M3
The open-association confirmation rule could thrash a healthy link. A question whose answer never comes - a host pinging or ARPing a peer that is switched off, DNS with no upstream, TCP retransmissions or a RST to a peer that has gone - failed the association as Unconfirmed, and on_association re-armed the judge on every re-join, so the link went down every ~5 s plus backoff and join, dropping host traffic each time. - The verdict is now a bounded backstop: at most one per BSS until an association on that BSS is confirmed. After a verdict the re-joined association on the same BSS is not judged (g_strike); a unicast reply, or an association on a different BSS, lifts the strike. The cost is stated where the rule is: a second unheld association on the struck BSS is not found by this rule. - A TCP segment is a question only as a SYN (SYN set, ACK clear). The old comment's "a bare ACK only follows data this station received" was false across a re-join. - A question, a probe and a nudge count only once enqueue() accepted them; one the 128-deep queue dropped never reached the AP. - g_unconfirmed is folded into g_judge. Headless cells: fewer than three questions are not judged, a broadcast from the AP does not confirm, what is and is not a question (TCP SYN vs SYN-ACK/ACK/data/RST, ICMP echo vs reply/unreachable, non-first fragment, DNS vs other UDP, unicast ICMPv6 echo/NS vs reply/multicast), one strike per BSS and its lift, the strike is per BSS, a dropped frame is not counted. The multicast-chatter cell now sends real IPv6 multicast echo requests; the reply cell sends real echo requests and judges across a full window; the idle-burst cell runs the loop while idle. Checked by 21 mutations of sta_client.cpp on a scratch copy; all are killed. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VNC8xhn1rNCi5t6uLvE6M3
Each station harness's cleanup set CLEANED=yes first, and the INT trap was `cleanup; exit 3`. A second INT during cleanup's sleeps and kills re-entered it, returned at once and exited, leaving the AP phy in the netns, the netns itself, NetworkManager unmanaged and the DUT not re-enumerated. cleanup now ignores INT/TERM as its first line in sta_client_onair, realtek_station_onair, mt7612u_sta_identity, mt7612u_sta_autoack and mt7612u_sta_uplink. Ignored rather than deferred, so the children it starts (the sleep between the two `authorized` writes) ignore them too. mt7612u_ap_onair also runs cleanup between cells, so there the ignore sits in the INT handler and the EXIT trap, and a between-cell cleanup runs with INT/TERM ignored and the handler restored after it. The same harness's reap dropped every pid from KIDS, a KILLed survivor included. In CELLS=all, a between-cell cleanup that returned 1 led to the EXIT trap's cleanup, whose reap saw nothing, returned 0, and re-enumerated the AP under the still-running process. A child still alive after its KILL now stays in KIDS, so every later reap returns 1 and no later cleanup touches the adapter. Checked off-device: a cleanup of the same shape hit by INT, INT, TERM runs to the end and exits 3; reap with a simulated unkillable child returns 1 and keeps it, and a later reap returns 1 again. Refs OpenIPC#467. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VNC8xhn1rNCi5t6uLvE6M3
The lib header said the adapters were "exclusive anyway". They are not: the OUT lock keeps apart only runs that share an OUT, and an adapter is held only while a devourer process has it open, which nothing does between two gates. A second run with its own OUT found interface 0 unbound, took the DUT, and its hand-back toggled `authorized` under the first run. sta_dut_take now refuses when interface 0 has no driver bound (another run, or a crashed one, holds it; the message says how to re-enumerate it) or is bound to anything but mt76x2u (usbfs: a devourer process has it open). The header now says what the lock does and does not cover. A host that runs without mt76x2u loaded is now refused rather than taken. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VNC8xhn1rNCi5t6uLvE6M3
Stop() cleared a still-armed station before closing, "because the chip keeps its registers across a close" and a managed filter would reach whoever opened the adapter next. It would not: every bring-up rewrites the receive filter (the initvals, then mt_mac_start()), and so does mt76, as docs/mt7612u-station-identity.md already said one sentence later. As a second chance for a failed clear it buys nothing for the same reason, so the hunk is dropped and Stop() is master's again; the doc now says why it does not clear. StartRxLoop logged "monitor RX" even when an armed station kept the managed filter (the monitor request is recorded, not installed). It now says which filter is in force. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VNC8xhn1rNCi5t6uLvE6M3
The open cell reported every Unconfirmed verdict as INFO "recovered", including one that hit an association hostapd had already logged as AP-STA-CONNECTED - a false positive that cost the user a re-join, and the on-air oracle for the confirmation rule. hostapd logs AP-STA-CONNECTED once per association it holds, so at most (associations - held) verdicts can be genuine. More than that is a FAIL naming both counts; the genuine "the AP lost us" recovery stays INFO. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VNC8xhn1rNCi5t6uLvE6M3
The last staid case checked that a clear after a responder dropped the arm "verifies the monitor receive filter". The drop had already written and verified that value, so the read-back held whether or not the clear wrote anything. The register is now poisoned with the managed value first, so only a clear that re-writes the pre-arm filter passes. On the SKIP path the clear still runs, unchecked, as cleanup. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VNC8xhn1rNCi5t6uLvE6M3
sta_unicast_inject.py's seq0 and sta_client_onair's two streams at 0 and 2048 were justified as keeping duplicate detection from merging them. The injected frames never set Retry, so 802.11 duplicate detection should not merge them in the first place. The ranges stay - the benched configuration used them - but both comments now call them a precaution rather than a need. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VNC8xhn1rNCi5t6uLvE6M3
…inds - sta_client_onair checked FW_DIR alone, but the library's resolve_fw_dir falls back to /lib/firmware/mediatek and then ./firmware. The preflight now takes the first of those that holds both blobs, exactly as the library does, and requires that one to be readable; with none, the refusal names FW_DIR. Header and docs/station-client.md say so. - sta_fw_readable said "only mt7662.bin.zst is there" even when an empty or unreadable .bin was there too. A missing .bin and an unusable one are now told apart, and a missing one reports whichever compressed copies sit beside it (.zst, .xz, .gz) as "compressed firmware". - mt7612u_sta_uplink.sh gets the "Exit status:" header line the autoack and AP harnesses have, placed after its Env line. Checked on fixtures under bash and dash: .zst-only and .xz-only refused by name, an empty .bin (with or without a .zst beside it) refused as unusable, a missing directory refused, a good directory and a symlink to it pass, and so does this host's /lib/firmware/mediatek. Refs OpenIPC#467. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VNC8xhn1rNCi5t6uLvE6M3
shellcheck 0.11 reports the new INT handler as never invoked (SC2329, SC2317 before 0.10). It is reached through `trap on_int INT TERM`. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VNC8xhn1rNCi5t6uLvE6M3
One strike per BSS could leave a held-but-dead association dead for the life of the process. Against an MT7612U AP that holds the association's TX status, the AP silently drops the station's data, never deauthenticates it, and keeps beaconing. A second such association on the struck BSS was never judged, and nothing else ended it. The strike is now a backoff. After n consecutive verdicts on a BSS (g_strikes), the next association on it is judged only once kConfirmMs * 2^n has passed since it was made: 10 s, 20 s, 40 s, 80 s, then the 2-minute cap (kStrikeCapMs). Questions asked inside the backoff are not counted, so the window opens at a question asked after it. A unicast reply, or an association on a different BSS, resets the count; a broadcast does not. A dead peer still costs at most one re-join per backoff period, and a dropped association is still found. Also: - A static_assert pins 3 questions / 5 s / the 2-minute cap to what docs/station-client.md states. - The association and verdict lines carry `at=`, the wall-clock time in hostapd -t's form, for the on-air harness to order them against the AP's log. Headless cells: - test_a_struck_bss_backs_off: the backoff values and cap; no judgement and no counting inside the first backoff; judged right after it; the second backoff twice as long; a reply resets it. - test_a_group_frame_does_not_reset_the_backoff. - test_the_strike_is_per_bss, reworked. - test_a_truncated_tcp_header_is_not_read: the frame sits in an exact allocation, so a missing l4n bound reads past it under ASan. 29 mutations of sta_client.cpp on a scratch copy are all killed. Two of them (the threshold and cap constants) are killed at compile time by the static_assert. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VNC8xhn1rNCi5t6uLvE6M3
- New headless cell test_the_stamps_are_wall_clock_microseconds. It
captures stderr across an association and a verdict, and requires
both lines to carry at=<sec>.<6 digits> within 5 s of time(NULL).
CLOCK_MONOTONIC, millisecond precision, and dropping at= from either
line are now killed.
- The verdict path no longer re-checks the BSS before counting a
strike; on_association already resets the count for a new BSS.
- docs/station-client.md:
- quotes the association and verdict lines with at=;
- describes the open cell's ordering check;
- adds two limits: the backoff remembers one BSS, so a station
alternating between two BSSes of one ESS resets it each time (a
verdict per cycle of the 5 s window, the re-join backoff and the
handshake); and sta_dut_handback rebinds mt76x2u even when the run
took the DUT unbound.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VNC8xhn1rNCi5t6uLvE6M3
…L comes first verdicts_scored's positive control took the LATEST CONNECTED in [previous verdict, next association]. So the next association's CONNECTED satisfied an earlier row, and that row's own early CONNECTED was never compared. Flash's input - association 1 at 1005, association 2 at 1010, a verdict at 1015, CONNECTEDs at 1004.9 and 1009.9 - scored INFO. Each association now owns a window: its at= to its verdict, or to the next association. Every own CONNECTED in hostapd's log is accounted for: - inside a verdict's window: that verdict FAILs; - the first one inside a window without a verdict: that association's own, the positive control - counted once per association, so one CONNECTED cannot satisfy several rows; - outside every window, or a second one in a window without a verdict: ignored if hostapd logged DISCONNECTED for us before the next association (an episode the re-join ended - the late release); otherwise it was stamped before an association's at=, the ordering premise fails on this rig, and the verdicts are INCONCLUSIVE. Flash's input is INCONCLUSIVE. It is not a FAIL: once the rig stamps first, a CONNECTED before an at= cannot be pinned to one association. Also: - BAD is tested before STAMP, so a malformed stamp on one row no longer hides a FAIL on another. - Records shorter than two fields are skipped ($(NF-1) on a blank record is fatal in gawk). - A missing parse result while the station log shows verdicts is INCONCLUSIVE. - docs/station-client.md describes the windowed pairing. 24 fixtures, run under gawk: - FAIL: held inside, multi (verdict 2 only), epoch 1 us, -dd double stamp, a FAIL beside a garbage stamp, blank hostapd lines. - INFO: late release in order, two controls, and unheld/held/unheld. - INCONCLUSIVE: Flash's shift, the held-early race, an early control, unheld/held-early/unheld, no control, missing/garbage stamps, a verdict before its association, a ledger mismatch, a missing ledger. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VNC8xhn1rNCi5t6uLvE6M3
The at= cell redirected stderr without checking dup()/dup2(), and required the stamp within 5 s of time(), which a loaded or sanitised CI box can miss. The redirect and the restore are now checked. The window is +/-60 s: the point is wall clock rather than monotonic, which differ by about the epoch. The 6-digit check is unchanged, and the four stamp mutations are still killed. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VNC8xhn1rNCi5t6uLvE6M3
…is not cleared A window without a verdict and without a CONNECTED of its own could claim the next association's early-stamped CONNECTED as its control. That happens when association k-1 was never held and ended in a deauthentication re-join, which leaves no verdict. Association k's false-positive verdict then had an empty window and scored INFO. New conservative rule: for each verdicted association, if the last hostapd event for our address before its at= is a CONNECTED with no DISCONNECTED after it, hostapd still held us when the association began. That CONNECTED may be its own, stamped early, so the verdicts are INCONCLUSIVE. The clean recovered path - late release, then DISCONNECTED, then the new association's CONNECTED after its at= - stays INFO. Also: - Our address is matched case-insensitively as the token after the event name, not as $NF (newer hostapd appends auth_alg=), and only on the cell's AP interface. - An empty at= is a stamp error. - The comment records two fail-safe quirks: mawk reading an empty hostapd log as station rows, and duplicated lines. 30 fixtures; gawk, mawk and busybox awk agree on every one. The three hole shapes are INCONCLUSIVE. Also: an upper-case MAC with a trailing auth_alg= FAILs; our MAC on another interface is ignored; an empty at= is INCONCLUSIVE; every earlier fixture is unchanged. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VNC8xhn1rNCi5t6uLvE6M3
The held-at-start rule takes the last hostapd event in log order before an association's at=. If hostapd's wall clock steps back, a DISCONNECTED later in the file can be stamped earlier than the CONNECTED before it. The rule then reads "not held", and a false positive scores INFO (fixture clock_back). If hostapd's stamps for our address are not monotonic in file order, the verdicts are now INCONCLUSIVE, with a message saying the AP clock stepped. 31 fixtures; gawk, mawk and busybox awk agree on every one. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VNC8xhn1rNCi5t6uLvE6M3
PR Summary by QodoExtend station coverage, AP recovery, and MT7612U managed filtering
AI Description
Diagram
High-Level Assessment
Files changed (29)
|
Code Review by Qodo
1.
|
…ociation Qodo on OpenIPC#471: rx_frame() cleared g_judge and reset the backoff on any unprotected frame addressed to us. A QoS Null (fc0 0xc8) does reach that path: is_qos_data() is (fc0 & 0x8c) == 0x88 (src/sta/Dot11.h:774), which admits the no-data subtype, and the gate at tests/sta_client.cpp :571 passes it. An AP sends a QoS Null for power-save or keepalive probing whether or not it forwards our traffic, so an AP that drops our data could still "confirm" the association. A plain Null (0x48) never got there: it is neither kFcData nor QoS data. The lift now needs a data-bearing frame: no-data subtype bit (0x40) clear, and a body. New cell test_a_null_frame_does_not_confirm covers a Null, a QoS Null, and a QoS Null with trailing bytes - each must leave the association to be judged Unconfirmed. Three mutations are killed: the to_us test dropped, null frames confirming, and the subtype check dropped. 35 of 35 in the set. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VNC8xhn1rNCi5t6uLvE6M3
…tarts or moves under it Qodo on OpenIPC#471: run_reconnect ignored a failed `sta_pid_kill hostapd`, and sta_pid_kill deleted the PID record before returning 1 for a process still alive after 10 s. So a stuck hostapd survived untracked, a replacement was started on the same interface, and cleanup later moved the AP phy and netns out from under the original. - sta_pid_kill keeps the record when the process survives. Every caller was checked; a kept record only means a later call (cleanup) tries again, which every caller tolerates. New sta_pid_kill_hard escalates to KILL, and new sta_pid_live asks whether a recorded process still runs. - sta_client_onair: - every hostapd stop is sta_pid_kill_hard; - ap_up refuses (the cell INCONCLUSIVE, the reason in the cell's hostapd log) while a recorded hostapd lives, so no replacement ever starts beside it; - run_reconnect scores INCONCLUSIVE and does not restart the AP when the old hostapd outlived TERM and KILL; - cleanup leaves the AP phy in the netns and the netns in place when hostapd outlived both, and prints the recovery commands - the same survivor rule as mt7612u_ap_onair's reap. - mt7612u_sta_identity and realtek_station_onair escalate the same way and do not re-enumerate the AP under a surviving hostapd. - The autoack/uplink trap comments no longer say sta_pid_kill forgets a PID. Checked off-device: - A TERM-ignoring stand-in: sta_pid_kill returns 1, the record is kept, ap_up refuses; sta_pid_kill_hard then ends it and drops the record. - A simulated KILL-resistant one: sta_pid_kill_hard returns 1, the record is kept, ap_up refuses. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VNC8xhn1rNCi5t6uLvE6M3
… is not live - test_a_null_frame_does_not_confirm gains a Data (0x08) and a QoS Data (0x88) frame addressed to us with an empty body. Neither may confirm. Without them, dropping the `len > hlen` half of the lift survived; the mutation is now killed. - sta_pid_alive returns "not live" for an empty or non-numeric PID. Before, an empty record read /proc//stat, which is /proc/stat, and counted as running. Checked under dash and bash. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VNC8xhn1rNCi5t6uLvE6M3
|
@josephnef - Qodo round addressed (407ea6d, c54226e, af19b9c): a (QoS) Null no longer confirms an open association, and a hostapd that outlives its kill keeps its PID record, is never replaced or moved under, and scores INCONCLUSIVE. ctest 83/83 normal and ASan+UBSan, 35/35 mutations killed. Re-benched on air; the description is updated with the numbers, including one intermittent on the MT7612U-station row that this diff does not cause (listed as a follow-up). 🤖 Generated with Claude Code |
josephnef
left a comment
There was a problem hiding this comment.
Reviewed af19b9c on this bench (ch6). The library delta is sound; one harness bug and the doc fixes are what I am asking for.
Library. The arm owns the receive filter with a read-back on every write, the clear restores and verifies (and keeps the arm recorded on a miss so a retry works), the stranded state covers an undo that did not read back, mt7612u_sta_rx_filter_request records a monitor request while armed/lost/stranded and installs the managed filter only while armed, and the drop/restore in mt7612u_station_identity_check writes the filter outside the io-error accumulator. StationSm::link_lost is Connected-only and goes through the ordinary fail(); every Failure switch site carries the new enumerator. ctest 83/83, no new warnings.
Hardware (tests/sta_client_onair.sh, all cells):
- RTL8812CU station (3-2.4) vs MT7612U AP (4-2.3.2, mt76x2u): 28/28.
- MT7612U station (4-2.3.2) vs RTL8812BU AP (4-2.3.3, rtw88, modprobed explicitly - this host blacklists rtw88 autoload): run 1 28 pass, 1 FAIL; a
wpa2 noarmrepeat 11/11.- The FAIL is
wpa2: ledger associations=2. hostapd's Group Key Handshake 1/2 went unanswered four times (TX status ack=1, ack=1, then ack=0) in the seconds the injection monitor vif was being torn down, the AP deauthenticated, the station re-joined after a handshake-timeout. That is the intermittent your description reports on this row, with a different signature (rekey not answered, not beacon loss). Worth looking at whether INJECT_S overlapping REKEY_S is what provokes it, and either not overlapping them or scoring the case. - The managed-filter witness passed both runs: own-addressed 775/863 and 875/875 arrived, foreign not-for-us 0/642 and 0/713, with the
noarmcontrol seeing the foreign stream.
- The FAIL is
Requested:
- The Realtek DUT take path has no live-holder gate (inline).
docs/mt7612u-station-identity.mdstill says the managed filter is TBD on air, and carries history/issue refs in prose (inline).
Nit inline: proc_running duplicates sta_pid_alive without the empty-PID fix. Two wording notes, no change needed: the description says "3 questions within 5 s of the first" while the code is >= 3 questions AND >= 5 s since the first (the doc matches the code); and the nudge is called a "directed probe request" but build_probe_req addresses it to broadcast with the SSID element, so it is SSID-specific rather than directed.
🤖 Generated with Claude Code
The Realtek take in sta_client_onair.sh (and every sta_dev_record user: realtek_station_onair.sh's DUT and peer, the MT7612U harnesses' peer) had no live-holder gate. sta_dev_unbind_wifi skips an interface with no wireless netdev, which is exactly what a libusb-claimed interface looks like, so a DUT another devourer process held passed, and cleanup's sta_dev_handback then toggled `authorized` under that live process. sta_usb_unheld refuses an adapter with an interface bound to usbfs or a process holding its /dev/bus/usb node. sta_dut_take and sta_dev_record both call it. sta_client_onair.sh marks the Realtek DUT opened only once its unbind has succeeded. sta_usb_holder reads `ls -l /proc/*/fd` instead of `find -lname`: busybox find has no -lname, and the swallowed error read as "nothing holds it". GNU find -samefile is no substitute, because it holds the node open itself. The AP paths need no gate: every AP guard requires a wireless netdev on the adapter, which a usbfs-claimed interface does not carry. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VNC8xhn1rNCi5t6uLvE6M3
sta_client_onair.sh and realtek_station_onair.sh each carried a proc_running that duplicated sta_pid_alive without its empty-PID guard: `proc_running ""` reads /proc//stat, which is /proc/stat, and returns true. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VNC8xhn1rNCi5t6uLvE6M3
A bench run of the wpa2 cell failed `ledger associations=2`. hostapd's Group Key Handshake 1/2 went unanswered four times while the injection's monitor vif was being torn down; the AP then deauthenticated and the station re-joined. With the defaults the overlap was structural. The group timer (20 s) starts with the AP. The injection began after the four-way and a 6 s ping, and ran 10 s, so it ended within a second of the first group rekey. The injection now runs straight after the four-way, before the ping, and only when hostapd's own -t stamps show it ends, with 5 s of margin for the vif and the early group timer, before both first rekeys: REKEY_S after AP-ENABLED and PTK_REKEY_S after the station's last four-way. Otherwise it is skipped and the filter check is INCONCLUSIVE, naming the knobs. The REKEY_S default moves from 20 to 30 s so that the defaults leave room. The cell still waits for both rekeys and pings after them. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VNC8xhn1rNCi5t6uLvE6M3
…no history
The witness section said "On-air numbers: TBD" and listed the managed
filter under a live association as not established. It is measured. An
MT7612U station against an RTL8812BU AP on two benches gave own-addressed
882/882, 943/943, 775/863 and 875/875 arrived, and foreign not-for-us
0/761, 0/927, 0/642 and 0/713. In every run the noarm control saw the
foreign stream. The numbers go into a table, and the TBD bullet goes.
Two passages narrated history: a retraction ("an earlier version of this
page said") and "used to run promiscuous ... (issue OpenIPC#461)". Both now state
the current behaviour: the PROMISC bit's decode, and why the armed station
runs the managed filter. The provenance stays in git.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VNC8xhn1rNCi5t6uLvE6M3
build_probe_req addresses the probe to broadcast and carries the SSID element, so it is SSID-specific, not directed. The comments, the docs and one check message use that term. Behaviour is unchanged. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VNC8xhn1rNCi5t6uLvE6M3
…tors Marking the DUT opened only after sta_dev_unbind_wifi succeeded left a driverless adapter in two cases: an unbind cut short by Ctrl-C during its sleep, and an adapter with two netdevs that frees one and then fails. In both, .opened_dut was absent, so the hand-back skipped the `authorized` toggle. The mark goes back before the unbind. sta_dev_record now refuses a held adapter, so the toggle cannot land under a live process, and a toggle on an adapter its kernel driver still holds is harmless. The injection guard said the injection "ends at least 5 s before" the rekeys. What the code checks is now + INJECT_S + margin < the first rekey, and that margin must also absorb the vif add/delete and the injectors' start. The injectors are now killed at INJECT_S + 2 s (KILL 1 s later) instead of + 10 / -k 5. The margin rises to 8 s, so it covers that bound, the vif, and a remainder before the rekey. The comments and the doc say exactly that. The identity doc's measurement table names each row's head, and notes that every row ran the earlier after-the-ping schedule with REKEY_S=20. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VNC8xhn1rNCi5t6uLvE6M3
…on schedule The table gains three bench-A rows from 42fab15 and 232a631. In these runs the injection starts straight after the four-way, clear of the rekeys, with REKEY_S=30. Own-addressed 833/833, 751/751 and 746/746 arrived; foreign not-for-us was 0/721, 0/683 and 0/684. The noarm control saw the foreign stream each time (own 746/746, 944/944, 821/821). A schedule column and a noarm column are added. The note says which heads ran which schedule. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VNC8xhn1rNCi5t6uLvE6M3
… own skip The managed-filter table had the 42fab15 row and the second 232a631 row swapped. Each row now matches its own log. The table also cites 0fa46cc's held PASS (943/943, 0/609) in place of the pre-rebase 4335ee4. It fills in the noarm counts that were recorded: 0fa46cc 724/740, c54226e 728/739. Bench B's single control figure (612/706) covered both of its runs, so it now sits in the note, not on one row. The unarmed control counts 9-31x as many foreign frames as were injected, while the own stream arrives 1:1. The AP very likely retransmits frames nobody acknowledges. The doc says so. The injector's docstring and the harness comment had claimed the frames never set Retry and that mac80211 injects them no-ack; both claims now stay within what the counts allow. rekey_clear returns its own codes for an unreadable log (2) and for a missing AP-ENABLED or four-way stamp (3). The skip names that case, not "would not end before the first rekey". The log is checked in the shell, because busybox awk exits 1 on a missing file. REKEY_S and PTK_REKEY_S must be at least 1: 0 switches hostapd's rekey off. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VNC8xhn1rNCi5t6uLvE6M3
|
@josephnef - thanks for benching it. All addressed in 26db041..ddec632:
On your
With this schedule, Our earlier c54226e 🤖 Generated with Claude Code |
josephnef
left a comment
There was a problem hiding this comment.
Re-reviewed ddec632. Everything I asked for is in: sta_usb_unheld on every DUT and peer take (and the busybox-safe holder scan), sta_pid_alive everywhere, the on-air table in the identity doc, no history wording left in the added text, and the injection fitted before the first rekey. ctest 83/83. The rekey FAIL is gone here: two further runs of the MT7612U-station row both read one association, three rekeys answered.
But the reordering brings a new FAIL on this bench, repeatable 2 for 2 (one full row A, one wpa2 noarm repeat):
FAIL wpa2: four-way completed, ping 6 packets transmitted, 5 received, +1 errors, 16.6667% packet loss
ping_wpa2.txt: seq 1 "Destination Host Unreachable", seq 2 at 464 ms, seq 3-6 at 5-17 ms. The ping now starts about 9 s after the own-stream injector finished. The noarm cell's 60-ping window in the same runs also shows +1 errors (58/60). The managed-filter check itself passes both times (own 938/938 and 835/835, foreign 0/576 and 0/708), the ping after the rekeys is 0% loss, and the earlier ordering (ping before injection) had 0% loss on this row every time.
Reading: the AP is still retransmitting the unacknowledged foreign stream when the ping starts - your own sta_unicast_inject.py note says the unarmed station sees 10-30x the injected foreign frames (6030 for 770 here), and the AP (rtw88 8812BU) is on a single hardware queue - so the ARP reply and first echo sit behind those retries. A hypothesis, from timing only; I did not capture the AP's queue.
Ask: make the post-injection ping independent of the AP's retry backlog - either a settle before ping_ap that is tied to the foreign stream (the injector's frame count times the AP's retry limit at the lowest rate, or a poll until the station's not-for-us counter stops moving), or keep the injection before the rekeys but after the first ping with REKEY_S raised to fit (rekey_clear already refuses when it does not). Inline at the reordering.
🤖 Generated with Claude Code
Injected before the ping, the managed-filter stimulus cost the wpa2 cell its first echo on a reviewer's bench, 2 of 2 runs: seq 1 "Destination Host Unreachable", seq 2 at 464 ms. The noarm window lost one of 60. The rtw88 AP keeps retransmitting the unacknowledged foreign stream (6030 seen unarmed for 770 injected), and the ARP and the first echo sit behind that backlog. Ping-then-inject was clean in every run. So the injection runs after the ping again, in wpa2 and noarm alike. It is still gated by rekey_clear. The defaults are sized for the longer case, noarm: the four-way ~2 s after AP-ENABLED, a warm-up ping of up to 3 s, a 30 s PING_S window, then INJECT_S + 8 = 18 s. That is about 53 s from AP-ENABLED and 51 s from the four-way. REKEY_S=90 and PTK_REKEY_S=80 leave about 30 s for a slower join or host. The only ping after an injection is wpa2's ping after the rekeys, about a minute later. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VNC8xhn1rNCi5t6uLvE6M3
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VNC8xhn1rNCi5t6uLvE6M3
|
@josephnef - moved the injection back after the ping in a66f659, for both On our bench, two runs of the MT7612U-station row on a66f659 read 🤖 Generated with Claude Code |
josephnef
left a comment
There was a problem hiding this comment.
Re-reviewed fba6700. The injection runs after the cell's ping again, for wpa2 and noarm, with the rekey defaults at 90/80 so rekey_clear still fits it before the first rekey, and the station's run budget scales with them. The post-injection ping FAIL is gone on this bench.
Hardware on fba6700, MT7612U station (4-2.3.2) vs RTL8812BU AP (4-2.3.3, rtw88), ch6:
- all cells: 29/29.
wpa2ping 6/6 before and after the rekeys, one association, three rekeys answered;noarm60/60; managed filter own 907/907, foreign 0/644 with thenoarmcontrol seeing the foreign stream. wpa2 noarmrepeat: 11/11, ping 6/6 and 60/60, managed filter own 885/885, foreign 0/859.
ctest 83/83. Earlier on this bench: RTL8812CU station vs MT7612U AP 28/28 (ddec632's harness, library unchanged since).
🤖 Generated with Claude Code
@josephnef - the station half of the #461 / #467 follow-ups, ready for review. The TX half is #470.
What changed
The station client (#464, #465) now runs on every die that reports
station_mode_ok, survives its AP restarting, and on MT7612U it is no longer promiscuous while armed. It also closes the station-harness items of #467.Refs #461, Refs #467 (partial: the harness items; see Follow-ups).
Library
sta: StationSm::link_lost. A caller's liveness check gets back into the ordinary failure path: fromConnectedit fails the association asFailure::Unconfirmed(no AID, queue cleared, like any failure); in any other state it does nothing. Association Responses that arrive when none is awaited are now counted (rx_assoc_repeat) instead of being dropped silently. cteststation_sm:test_link_lost.mt7612u: the station arm owns the receive filter. Addresses the managed-filter item of Follow-ups left open by the station identity seam (#460) #461.SetStationIdentityinstallsMT_RX_FILTR_CFG_MANAGED(0x00015f97, the value every station cell indocs/mt7612u-station-identity.mdmeasured;regs.hnow names its bits). It replaces the monitor filter thatStartRxLoopinstalls, and the arm counts only once the write reads back.ClearStationIdentityputs the replaced value back and returns true only once that reads back. A failed clear keeps the arm recorded, so a second clear retries.mt7612u_set_monitor_rx()records its request instead of installing it, so the arm does not depend on whether it came before or afterStartRxLoop.Stop()does not clear a still-armed station. Every bring-up rewrites the filter (the initvals, thenmt_mac_start()), and so does mt76, so a filter left armed at a close reaches no later opener.StartRxLoop's log names the filter in force at RX start, andSetStationIdentitylogs the switch to the managed filter (the usual station arms after RX start).mt7612u_clear_station_identity()now returnsint.IRadio.handAdapterCaps.hchange comments only; no Realtek backend is touched.StationIdentity.hand is pure; ctestmt7612u_station_identitycovers it, including the bit decode. The hardware gatemt7612uprobe staidchecks the filter across arm, re-request, refusal, clear and drop.Station client and harness (tests only)
tests/mt7612u_sta_onair.sh→tests/sta_client_onair.sh.sta_dev_record/_opened/_unbind_wifi/_handback), which replacerealtek_station_onair.sh's own copies. A DUT is still never re-enumerated whilesta_clientis alive.flock(1)on the OUT directory. The oldmkdir+ separate PID write could let a concurrent run read an empty PID, reclaim the live lock and share OUT.noarmis the arm's control: unarmed, the MAC does not acknowledge, so hostapd never lets the station in. On MT7612U,noarmis the receive filter's control. On both dies the clear must verify.reconnect/noreconnectcells. hostapd is stopped forDOWN_Sand restarted on the same BSSID; the variant runs withDEVOURER_STA_RECONNECT=0. The ledger's first line is the state the run ended in, taken before the teardown's leave, sonoreconnectendsFailed reason=...rather thanIdle.wpa2,noarm).sta_clientrefuses it on WPA2 and counts it (plaintext refused, new). If less than half of it arrives, the check is INCONCLUSIVE.not-for-usmust stay under 1% of the foreign stream. Unarmed: at least half of it must arrive.noarmcontrol of the same run has seen the foreign stream arrive; without it the check is INCONCLUSIVE.INJECT_S+ 8 s is still before both first rekeys, read off hostapd's own stamps:REKEY_Safter AP-ENABLED andPTK_REKEY_Safter the four-way. The injectors are killed atINJECT_S+ 2 s (KILL 1 s later; was + 10 / -k 5), so the 8 s cover them, the vif's add and delete, and a margin. Otherwise it is skipped and the check is INCONCLUSIVE, naming the knobs.REKEY_SandPTK_REKEY_Sdefault to 90 s and 80 s (were 20 and 25), sized for noarm, the longer cell: four-way ~2 s after AP-ENABLED, up to 3 s of warm-up ping, the 30 sPING_Swindow, thenINJECT_S+ 8 = 18 s - about 53 s from AP-ENABLED, 51 s from the four-way, leaving ~30 s for a slower join or host. The wpa2 cell now waits about a minute longer for its rekeys.sta_client). A station cannot see the AP's side, so an open association counts as unconfirmed until the AP's first unicast data frame to the station. A (QoS) Null carries no MSDU and does not count.unconfirmedand re-joined.static_assert.opencell FAILs a verdict on an association the AP held. Each association and verdict line fromsta_clientcarries anat=wall-clock stamp, and a verdict FAILs if hostapd (-t) logged AP-STA-CONNECTED for our address between the association it ended and the verdict. A CONNECTED logged after the verdict does not count; the re-join's own probe can release a held status that late.at=to its verdict or the next association:at=, so the verdicts are INCONCLUSIVE rather than cleared.at=is a CONNECTED with no DISCONNECTED after it. That CONNECTED may be its own, stamped early. This closes the case where an earlier window has no CONNECTED of its own (an association hostapd never held, re-joined after a deauthentication, which leaves no verdict) and would otherwise claim the next association's early CONNECTED.auth_alg=field is fine. On a rig where hostapd stamps first, expect INCONCLUSIVE rather than INFO whenever a verdict fired.sta_client.cppon a scratch copy, all killed (see Verification).sta_client). The moment an Association Response is accepted, open or WPA2, the station sends one SSID-specific probe request (to broadcast, carrying the SSID element). On WPA2 a second one follows if no EAPOL has arrived 1 s later, and none after that. The nudge does not retune and does not touch liveness state (pinned headless). See "AP quirk" below for why.PING_S) replace six-ping link reports. Also added:HOSTAPD_DEBUG=1(hostapd-dd), the dmesg tail of a failed cell, andAP_OFDM_ONLY=1.Harness follow-ups from #467 (tests only)
mt7612u_sta_identity.sh: a gate that could not measure now reads as INCONCLUSIVE. An overrun BSSID gate setr_bss=2, but the verdictcasehad only0,3and*, so the operator read "did not pass" for a run the exit code called INCONCLUSIVE. The probe-response gate had the same gap (r_ack=2, arm C never reported), and both now have a2)branch.sta_fw_readablein the lib checks that both MT7612U blobs are present, readable and non-empty, and names the.bin.zst-only case when that is what it finds.sta_fw_linkruns it through the link it made or found, somt7612u_sta_identity,_autoackand_uplinkrefuse the rig (exit 2) in seconds, before the DUT is taken. Before this, the uplink harness scoredABORTED the DUT did not confirm retry limit 15:with an empty reason.sta_client_onair.shruns the same check onFW_DIRfor an MT7612U DUT.mt7612u_sta_autoack,mt7612u_sta_uplinkandmt7612u_ap_onairexited 130. All six station harnesses now share one convention: 0 pass, 1 fail, 2 inconclusive, 3 interrupted. The autoack, uplink and AP headers now state it, and no caller of a station harness reads 130.mt7612u_ap_onair.sh:reapno longer leaves zombies. On the KILL path it clearedKIDSwithout waiting on anything. It now gives a KILL 2 s to land, then waits on every child that has exited. A child still running after that is not waited on, since that wait could block. The return value is unchanged.realtek_station_onair.sh:tail_msis clamped at 0. The finaltx.statstcan come a few ms before the lasttx.reportt. This never affectedlive.Harness hardening from review (tests only)
CLEANED=yesfirst, and the INT trap wascleanup; exit 3, so a second INT mid-cleanup returned at once and exited. That left the AP phy in the netns, the netns itself, NetworkManager unmanaged and the DUT not re-enumerated. cleanup now ignores INT/TERM as its first line. Inmt7612u_ap_onair.sh, which also cleans up between cells, the ignore sits in the INT handler and the EXIT trap, and a between-cell cleanup runs with INT/TERM ignored.mt7612u_ap_onair.sh: a child that outlives its KILL stays recorded. Before,reapdropped it, so the EXIT trap's later cleanup re-enumerated the AP under it. Now every later reap returns 1 and leaves the adapter alone.sta_usb_unheld): an interface bound to usbfs (a process has claimed it), or a process with the adapter's/dev/bus/usbnode open; the message names the PID.sta_dut_take(MT7612U) andsta_dev_record(a Realtek DUT or peer, insta_client_onair,realtek_station_onairand the MT7612U harnesses' peer) both run it;sta_dut_takealso refuses interface 0 bound to any driver other than mt76x2u. A Realtek DUT is marked opened before its unbind, so an unbind cut short (Ctrl-C, or one of two netdevs freed) is still handed back; the gate has already refused a held adapter, so theauthorizedtoggle never lands under a live process. The AP paths need no gate: every AP guard requires a wireless netdev, which a usbfs-claimed interface does not carry. Before, a second run with its own OUT could toggleauthorizedunder a live devourer process. An unbound interface that nothing holds is taken as it is: a devourer session detaches mt76x2u and never reattaches it, and a host may blacklist mt76x2u. The lib header no longer claims the adapters are exclusive, and says what it cannot see: another harness between two of its gates.sta_usb_holderreadsls -l /proc/*/fd. It usedfind -lname, which busybox find lacks; the swallowed error read as "nothing holds it". GNUfind -samefileholds the node open itself, so it is no substitute.proc_runningis gone fromsta_client_onair.shandrealtek_station_onair.sh: both call the lib'ssta_pid_alive, which has the empty-PID guard.sta_dut_handbackwaits for mt76x2u to bind again (up to 5 s, when it is loaded), so a back-to-back next run starts from a bound adapter.sta_client_onairtakes the first ofFW_DIR,/lib/firmware/mediatekand./firmwarethat holds both blobs, asresolve_fw_dirdoes.sta_fw_readabletells a missing blob from an unusable one and names any compressed copy (.zst,.xz,.gz).mt7612uprobe staid: the clear-after-drop check poisons the register first and reads the poison back, so it can fail. Before, the drop had already written the value it read back. A poison that does not stick is a SKIP, which counts as a failure like the gate's other SKIPs. A clear that missed is retried once, and a second miss is reported.Why
#464 could only take an MT7612U. On that part the arm writes no identity register, so neither the arm nor its control was ever measured on the dies where the arm writes registers. On the MT7612U itself, a station driven through
IRadioran the monitor filter: it received promiscuously, and the measured property "moving the port identity makes a station deaf" did not hold for it. And a station that cannot survive its AP restarting is not ready for real use.What is measured
Bench: one host (Linux 7.0), ch6, near field. MT7612U at 1-1, RTL8812CU at 5-1, RTL8812BU at 8-1.
Which head. The base full run is on 0fa46cc. af19b9c was re-benched after the Qodo round (below), here and on the reviewer's bench, as was c54226e, the same code on air before af19b9c's selftest and lib-guard commit. The last benched code is 232a631 (the review round on af19b9c: harness gates, the injection schedule), benched here, as was 42fab15, the same code before the opened-mark and injector-bound commit (below). The PR head is fba6700, a doc-only commit adding the newest runs to the identity table on top of a66f659, which changed the schedule after that and was benched here on the MT7612U-station row, twice:
wpa2ping 6/6 andnoarm60/60 both times, managed filter own 893/893 and 942/942 with foreign 0/796 and 0/789. One run was 29/29; the other 24 +noreconnectINCONCLUSIVE, the beacon-loss intermittent below (1 beacon received after keying; that cell does not inject). It moves the injection back after the ping, withREKEY_S/PTK_REKEY_Sdefaults of 90/80 (below). ddec632 before it added only an INCONCLUSIVE message of its own for a missing hostapd stamp, a check thatREKEY_SandPTK_REKEY_Sare at least 1, and doc and comment corrections. The reviewer's second bench ran ddec632. Between 0fa46cc and 2f6cffe only the open cell's verdict scoring (verdicts_scored), its fixtures intests/sta_client_selftest.inc, and docs changed, and the open cell was re-run on 2f6cffe. The held managed-filter counts are in the table indocs/mt7612u-station-identity.md.On air (
tests/sta_client_onair.sh, all cells, 0fa46cc):docs/mt7612u-station-identity.md). In every run thenoarmcontrol saw the foreign stream.REKEY_S=20: 0fa46cc own-addressed 943/943, foreign not-for-us 0/609 (control own 724/740); c54226e 943/943, 0/927 (control 728/739); af19b9c on the reviewer's bench 775/863, 0/642 and 875/875, 0/713 (one control figure for both runs: own 612/706).REKEY_S=30: 42fab15 746/746, 0/684 (control 821/821); 232a631 751/751, 0/683 (control 944/944) and 833/833, 0/721 (control 746/746). ddec632 764/764, 0/739 (control 870/870).REKEY_S=90/PTK_REKEY_S=80(the harness default): a66f659 893/893, 0/796 (control 720/943) and 942/942, 0/789 (control 700/744).open wpa2×3 (HOSTAPD_DEBUG=1, 0fa46cc): 13/13 each.mt7612u_sta_identity(0fa46cc): staid 22/22, rc 0. STAACK is INCONCLUSIVE and the BSSID gate is judged by the operator, both as documented.mt7612u_sta_autoack3/3.realtek_station_onair: 5/5 with the 8812CU as the DUT and 5/5 with the 8812BU; thetail_msclamp was exercised.mt7612u_ap_onairwith CELLS=all: 13/14. In thestopcell, the 8812BU witness scan did not see the re-arm phase's beacon once, although devourer logged "beaconing" for the re-arm. Re-running that cell 3× on 2f6cffe gave 4/4 each time. The library diff against the previous 14/14 run (6e7813e) is two log lines. Read it as an intermittent witness miss, not a pass.mt7612u_sta_uplink(FRAMES=20): FAIL, rc 1. Master 1134df9 fails identically on the same unit: the unicast arms get 0-7 of their 20 status entries and are UNSETTLED. This is the Follow-ups left open by the station identity seam (#460) #461 status loss inside thetxsgate, which this PR does not touch. mt7612u: no TX transfer timeout - a full ring refuses, it never cancels #470 fixes it: on that branch every arm reads 60/60. This harness's verdict depends on mt7612u: no TX transfer timeout - a full ring refuses, it never cancels #470, not on this PR.The open cell on 2f6cffe: MT7612U station 5/5, 8812BU station 5/5, and the 8812CU station 5/5 twice.
One 8812CU run caught the real case on air, against the MT7612U AP:
at=…74.513104) never got an AP-STA-CONNECTED from hostapd: the AP held its status.at=…82.126184) got AP-STA-CONNECTED at…82.127645, 1.5 ms after the station's own stamp, so the ordering control held on this rig.af19b9c (after the Qodo round).
sta_client_onair, all cells:HOSTAPD_DEBUG=1, four with a sniffer on a third radio. 1× 24 passed withnoreconnectINCONCLUSIVE.c54226e (the same code on air):
open wpa22× 13/13.wpa2FAIL andnoreconnectINCONCLUSIVE (see "Intermittent on the MT7612U-station row" below).noreconnectINCONCLUSIVE. That is the known MT7612U-AP hold of the association response's TX status ("AP quirk", below); withDEVOURER_STA_RECONNECT=0the station cannot re-join past it.mt7612u_sta_identity: staid rc 0.realtek_station_onair: 5/5 with the 8812CU as the DUT, 5/5 with the 8812BU.af19b9c on the reviewer's bench (ch6; RTL8812BU AP under rtw88, modprobed explicitly). ctest 83/83, no new warnings.
sta_client_onair, all cells:wpa2 noarmrepeat gave 11/11.wpa2: ledger associations=2. hostapd's Group Key Handshake 1/2 went unanswered four times (TX status ack=1, ack=1, then ack=0) in the seconds the injection's monitor vif was being torn down. The AP then deauthenticated, and the station re-joined after a handshake timeout.wpa2passed in every run since (below).noarmcontrol saw both streams (own 612/706).42fab15 and 232a631 (the review round on af19b9c).
sta_client_onair, all cells;wpa2passed in every run with the new injection schedule.open wpa22× 13/13; 8812BU station, MT7612U AP 28/28.mt7612u_sta_identity: staid rc 0.realtek_station_onair: 5/5 with the 8812CU as the DUT, 5/5 with the 8812BU.ddec632 on the reviewer's bench (second round). The rekey FAIL is gone. A new FAIL repeated 2 of 2:
wpa2ping 6 transmitted, 5 received, +1 errors (seq 1 "Destination Host Unreachable", seq 2 at 464 ms), and thenoarmwindow 58/60. The ping started ~9 s after the injectors ended, behind the rtw88 AP's retransmissions of the unacknowledged foreign stream (6030 seen unarmed for 770 injected). The managed-filter check passed and the ping after the rekeys was 0% loss. a66f659 injects after the ping again, the order that was clean in every earlier run.Intermittent on the MT7612U-station row. It was seen twice in 11 runs of that row: c54226e
wpa2and af19b9cnoreconnect. The three runs since (42fab15 once, 232a631 twice) were clean.wpa2FAIL, the station loggedlink lost: beacon-lost2 s after association 1, about 6 s before the injection started; the rekey completed and was answered.Against it: one run per cell (except where repeats are given), one rig, one channel.
What the Realtek
noarmcontrol scores. Unarmed, the MAC does not acknowledge own-addressed unicast, so the AP never sees its authentication response ACKed and never lets the station in. The cell scores:A completed four-way is a FAIL. The cell is INCONCLUSIVE unless the armed
wpa2cell of the same run got in, which is its positive control.AP quirk: an MT7612U AP holds the association's TX status
The intermittent open-association failure of an 8822C station against an MT7612U AP is AP-side. It is the same silicon behaviour as #461's next-submit hold (a frame's TX status is posted only when the next frame goes out), here in the kernel's mt76x2u.
hostapd's debug log of a failing run:
ack=1. By then hostapd no longer held the station ("handle_assoc_cb: STA ... not found").Supporting observations:
assoc_repeat=0).Bench,
open wpa2×5 per AP:unconfirmed=0every run); wpa2 3/5. The two failures are the hold above, which the open-only nudge did not cover.open wpa2×3: 3/3 clean (13/13 each) on 6e7813e and again on 0fa46cc,unconfirmed=0every run on 6e7813e.What it can't do
DUT_VID/DUT_PID, butsta_clientrefuses them unless they reportstation_mode_ok.noarmresult rests on hostapd's TX-status reporting through the AP's driver.rx.keep_corruptedapplies to the monitor filter only) and runs hardware duplicate drop, as mt76 runs a station.select_opennormally keeps to one BSS.sta_dut_handbackrebinds mt76x2u even when the run took the DUT unbound.Base
Rebased on master after #466 (1134df9). This PR renames
tests/mt7612u_sta_onair.sh, which #466 edited. The renamedtests/sta_client_onair.shcarries #466'sap_upblock (wait for the AP netdev, force it to managed, leave it up on a timeout) line for line, so the rename drops nothing #466 added. Two of this branch's commits duplicated that block for the old file name, and they were dropped in the rebase. Every other file's diff against master is the same as it was against #464.Follow-ups
mt7612u_sta_identity.sh); arguably INCONCLUSIVE.gate_txsstale-EXT over-attribution, thestale_settledcascade note, the Follow-ups left open by the station identity seam (#460) #461 multi-entry loss, themt7612u_sta_uplink.shdut_boundsizing) is in mt7612u: no TX transfer timeout - a full ring refuses, it never cancels #470.Verification
cmake -DDEVOURER_MT7612U=ON -DDEVOURER_REQUIRE_STA_CRYPTO_TESTS=ON:ctest83/83.-DDEVOURER_SANITIZE=address+undefined:ctest83/83, andsta_client --self-testrun directly under it prints no sanitizer report.mt7612u-only72/72,mt7612u+jaguar173/73,rtl8733b-only75/75,jaguar2-only73/73,jaguar3-only72/72.mt7612u_usb_ids_vs_mt76andmt7612u_initvals_generatedreport Skipped in every build here, and are counted in the totals. They need thereference/submodules.bash -npasses on every touched script (andsh -non the lib).shellcheck -x(0.11) adds no new finding class to any of them. What remains is theSC2329note on indirectly called functions, which these scripts already suppress under its older codeSC2317, plusmt7612u_ap_onair.sh'sSC2046/SC2015/SC2012, which predate this branch.sta_client --self-test, mutation-checked. 35 mutations ofsta_client.cpp, each compiled from a scratch copy, and all 35 are killed - 33 by a failing check and 2 at compile time by thestatic_assert. They cover:solicits_replybranch: TCP never or always a question, a SYN-ACK counted, the TCP length bound, ICMPv6 never counted, any ICMP type counted, the non-first-fragment check, the multicast-DA refusal, any UDP counted, ARP never counted, gratuitous ARP counted;at=stamps: CLOCK_MONOTONIC instead of the wall clock, millisecond precision, andat=dropped from the verdict line or from the association line.sta_fw_readableon fixtures, under bash and dash: refuses.zst-only and.xz-only directories by name, an empty.binas unusable, and a missing directory. It passes a good directory, a symlink to one, and this host's/lib/firmware/mediatek, which has both forms.reapwith a TERM-ignoring child: rc 1 and no zombies. With a simulated unkillable child: rc 1, and the child is kept, so a later reap returns 1 again. A clean reap still returns 0.verdicts_scored(the open cell's verdict check), on 31 fixtures, with identical results under gawk, mawk and busybox awk:-dddouble stamp, and only the held verdict of three.auth_alg=open.at=(the held-early race; a control whose CONNECTED is early; an association's CONNECTED landing in the previous association's window; three shapes of a verdicted association that began while hostapd still held us; an AP clock that stepped back), no control, missing, empty or garbage stamps, a verdict before its association, a ledger mismatch, and a missing ledger.rekey_clear(the injection's schedule) against c54226e'shostapd_wpa2.log, with a fixed clock, under gawk, mawk and busybox awk: 8/8. Clear with room; clear by 0.08 s and not clear by 0.42 s before the group rekey; the pairwise timer counted from the LAST four-way; no four-way for that station; an empty log; a missing log.summarizewith a finaltx.stats6 ms before the last report:tail_ms=0,live=1.Review record
josephnef on sta: station client on every station-capable die, AP restart survival, MT7612U managed RX filter #471 (CHANGES_REQUESTED at af19b9c), all addressed in 26db041..ddec632; his second round (the ping behind the injection's backlog) in a66f659. The library delta was found sound. Done: a live-holder gate on the Realtek DUT take, lifted into the lib for every DUT and peer take (the opened mark stays before the unbind: the gate closes the live-process case, and moving the mark would strand an adapter whose unbind is cut short); the identity doc's on-air numbers in place of TBD; the history narration and issue reference taken out of that doc;
proc_runningreplaced bysta_pid_alive, in two scripts. The two wording notes are fixed: the 3-question / 5 s rule, and "SSID-specific", not "directed". Hiswpa2FAIL (a group rekey unanswered during the injection vif's teardown) led to the injection being scheduled clear of the rekeys.Qodo on sta: station client on every station-capable die, AP restart survival, MT7612U managed RX filter #471, two findings, both held and fixed:
is_qos_data()admits the no-data subtype; a plain Null does not. Only a data-bearing frame now confirms.run_reconnectcould start a replacement beside it, and cleanup could move the AP phy out from under it. The record is now kept; the AP is not restarted or moved while it lives; the cell is INCONCLUSIVE.Independent verification of the Qodo fixes, two follow-ups, both done (af19b9c):
/proc//stat(that is,/proc/stat) and counted as running.sta_pid_alivenow treats an empty or non-numeric PID as not live.🤖 Generated with Claude Code
https://claude.ai/code/session_01VNC8xhn1rNCi5t6uLvE6M3