mt7612u: an 802.11 station identity seam on IRadio, implemented for the MT7612U - #460
Conversation
|
@josephnef PR4 of the station series, now that #452 and #454 are in. It is one commit on 101fdc9: the IRadio station identity seam, with the MT7612U as its first implementation. It builds on #452's retry-limit semantics: a station session has to set Before opening it went through two independent review passes, each checked against the tree, and it was re-verified on an MT7612U: |
PR Summary by QodoAdd MT7612U infrastructure-station identity support to IRadio
AI Description
Diagram
High-Level Assessment
Files changed (24)
|
Code Review by Qodo
1.
|
13eb6b9 to
87aef53
Compare
|
/review |
|
Code review by qodo was updated up to the latest commit 87aef53 |
87aef53 to
cc48be4
Compare
josephnef
left a comment
There was a problem hiding this comment.
Reviewed at cc48be4.
Verified here: configure with -DDEVOURER_MT7612U=ON -DDEVOURER_REQUIRE_STA_CRYPTO_TESTS=ON, build clean (only the pre-existing basic_types.h / Radiotap.c warnings), ctest 80/80 with mt7612u_station_identity registered and passing, make -C src/mt7612u check resolves 39 entry points. CI green on every job.
Not verified here: the on-air gates. The rig's CF-922AC (INVENTORY #17) is not plugged at the moment, so staid / autoack / identity / uplink could not be re-run on independent hardware. The bench record in the description is the only hardware evidence; it is honest about that (one unit, one peer, one run per arm, no arm through IRadio).
Library side: no findings. Checked the tri-state hand-off end to end (fail_post → unwind_identity → clear_ack_responder's own check at allow_restore=0 → the beacon's check at !sta_lost_before; idempotent, so the double call is correct), new mt7612u_dev{} zero-initialises sta, mt_rr_chk leaves *val untouched so the failed-read-vs-zero-address case really is caught, mt_io_restore keeps a beacon start's io-error delta honest, and the StartRxLoop thread/lock text matches what UsbTransport actually does in Async vs SpscFat mode.
One thing to fix before merge (test tool, not the library):
bringup.cppgate_sta: the arm loop'sif (g_stop) break;falls straight into the verdict block, which only testsany_beacon/any_to_us/unverified. A SIGTERM during arm B, after arm A already saw beacons and unicast, printsGATE STA: measuredand returns 0 with B–F never run.gate_txsandgate_tsfwrapreturn 3INTERRUPTED - no verdictfor this;gate_stashould too.tests/mt7612u_sta_identity.shfolds the gate's rc into its exit, so today a gate cut short by anything other than the harness's own Ctrl-C reads as a pass.
Nits, take or leave:
gate_norsp/gate_bssendiscardwait_ticking()'s return, so an interrupted 1 s dwell exits 0 with "done". Same rc-3 convention applies.gate_bssen's singlebadpath returnsrestored ? 1 : 2for both a failed register write (a defect) and the "BIT(16) would not stay set" case (genuinely inconclusive). Split them.mt7612u_sta_autoack.sh/_uplink.shcleanup:sta_pid_kill peer INTthensta_peer_handback— the peer PID was started inside a command substitution, so the lib'swaitreturns at once and theauthorizedtoggle can land while txdemo/rxdemo is still inside chip de-init. A boundedkill -0poll before re-enumerating closes it.- Same two scripts:
sta_peer_recordsetsSTA_PEER_IDbefore the peer is ever opened, so a failingsta_dut_take(exit 2) still bounces the driver of a Realtek adapter this run never touched. - All three harnesses:
[ ! -e "$ROOT/firmware" ]follows symlinks, so a dangling operator link is treated as absent, overwritten, and then deleted by cleanup as "ours".&& [ ! -L "$ROOT/firmware" ]keeps the stated rule. mt7612u_sta_lib.shlock:kill -0on a PID read from a reusedOUTcan match a recycled PID of an unrelated process and refuse the run until it exits. Failure mode is a refusal, never a wrong number, so this is cosmetic.
Interface shape looks right to me: the seam is separate from SetAckResponder for a measured reason, the not-ported defaults are the trivially-true ones, and station_mode_ok is gated on a bench cell rather than a code reading. Happy to approve once the gate_sta rc is in.
|
Hardware follow-up at cc48be4, second unit and second rig: MT7612U Comfast CF-922AC (
Two harness portability notes from this rig, both worth a line in the script header rather than code:
Neither changes the verdict above: still a comment pending the |
josephnef
left a comment
There was a problem hiding this comment.
Changing the status to changes requested: the gate_sta interrupt exit code (falls into the verdict block and reports measured, rc 0, when cut short mid-arm; convention is rc 3 INTERRUPTED) needs to land before merge. Details in my two comments above.
…he MT7612U IRadio gains SetStationIdentity(own, bssid) / ClearStationIdentity(): program the MAC for the STATION half of an infrastructure BSS. The contract at the declaration says why this is not SetAckResponder(bssid) (on the MT7612U that retargets the port identity a station needs left alone), requires the call after the RX loop is running, makes Clear report whether the rollback was verified, leaves a later port-0 claimant (ACK responder, beacon) to the backend, and states that transmission is not part of the arm: a station's unicast must request an ACK (build_stream_radiotap(mode, false)) and needs a nonzero tx.retry_limit, whose default of 0 sends each frame once. The not-ported defaults return false / true. StartRxLoop's doc gains the lock rule and its libusb mechanism: never make a device call while holding a lock the RX callback takes. AdapterCaps::station_mode_ok (adapter.caps "station_mode") gates callers: TRUE on the MT7612U only, FALSE (not ported) everywhere else. MT7612U: mt7612u_set_station_identity() writes NO register. It refuses unless `own` already is MT_MAC_ADDR and MT_AUTO_RSP_EN is set (both reads fail closed), and records the BSSID for the host. The decision is the pure StationIdentity.h, covered by ctest mt7612u_station_identity (built only with DEVOURER_MT7612U). After every write to MT_MAC_ADDR the arm is re-checked against what the register holds, tri-state: a verified move drops it with a WARN, an unreadable register keeps it with a WARN, and a beacon start that fails and unwinds the identity back restores the arm it dropped. Mt7612uRadio::SetStationIdentity warns when tx.retry_limit is 0. mt7612uprobe gains the staid/sta/staack/norsp/bssen gates; the APC slot is the station's (mt76 keys it on the station's own address - slot 0 for a factory address), every write is read back, and staid checks on the chip that arming and clearing write no register. An interrupted gate reports INTERRUPTED - no verdict (rc 3). The receiving gates tick the PHY once a second, turn the receiver off before the EP4 ring, read their receive filter back, and verify every restore. The harnesses (tests/mt7612u_sta_identity.sh, _autoack.sh, _uplink.sh, sharing tests/mt7612u_sta_lib.sh) write only to a private OUT, kill only recorded PIDs, hand every adapter back (the DUT to mt76x2u, the Realtek peer and the AP by a re-enumeration guarded on their recorded USB identity), guard the AP adapter, start the unicast injector only after the DUT's bring-up, and refuse a table the injector did not really feed. Measured (docs/mt7612u-station-identity.md; one DUT, channel 6, near field, driven by mt7612uprobe, not through IRadio): - Auto-ACK, asked of the transmitter (a Realtek peer's CCX reports): 100% at 0.45 mean retries over 1279 reports with nothing armed, against 0% / 12.00 retries for a destination nobody holds, the DUT absent, and MT_AUTO_RSP_EN cleared (one bit, same code path and filter). One peer, one run per arm. - MT_MAC_BSSID programmed WRONG: 5877 unicast frames vs 6250 with nothing programmed; the AP in the station's slot 0: 5878. Arm A's ~6% excess is unexplained beyond a first-arm pattern. A wrong BSSID in the station's slot with its enable bit set was still acknowledged 867/867 (one run, acknowledgement only). Earlier rows that wrote the "derived" slot used slot 1 - the AP-side rule, not the station's - and are withdrawn. On the station-slot gate (one run): A 6277, B-E 5846-5861, F (both WRONG) 6003 unicast frames - no gating, within the 4-7% first-arm spread. Runs that measured nothing trace to the rtw88 AP's transmit path stalling under the stimulus (inferred) - on a full-speed hub, and on the second unit's rig at a SuperSpeed root port too; the harness orders the stimulus after bring-up and refuses a starved table. - Moving MT_MAC_ADDR under the managed filter: reception 103 -> 0 frames. - Uplink (MT_TX_STAT_FIFO, peer an ACK responder): 200/200 at 0.0 retries vs a 0/200 full-ladder control that is UNSETTLED by construction and accepted only as a floor. Ran at the initvals' retry limit 15; the harness now sets that limit explicitly (re-run: 200/200 vs 0/157 settled at 16.0). Not a default library session. - A second unit (CF-922AC) reproduced staid 12/12, the auto-ACK claim, all three controls and arm E (1706/1706; 0% at 12.00; 1695/1695), the uplink (60/60 - but at 1.9 mean retries, against 0.0 on the first unit - vs 0/60) and the arm-C deafness; its BSSID table was lost to the AP stall and is not reproduced. Against it: the BSSID gate's first run had the monitor filter installed and is withdrawn, as is the probe-response auto-ACK method, whose control cannot move. Every cell is an unassociated station; no cell armed through IRadio; and StartRxLoop installs the monitor filter, so a station driven through IRadio runs promiscuous, not under the managed filter the cells measured. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VNC8xhn1rNCi5t6uLvE6M3
cc48be4 to
091ced5
Compare
|
Thanks for the review and the second-unit run. Everything is in 091ced5, and the "Maintainer review (josephnef)" section of the description answers each point. Blocking: Nits:
Header notes:
Your second unit is recorded side by side in the doc and the description. It reproduced Re-run on this head, on my rig:
After every harness, all three adapters were back on their drivers. |
josephnef
left a comment
There was a problem hiding this comment.
Re-reviewed at 091ced5. The blocking item and every nit are addressed; approving.
Verified on the second unit (CF-922AC, peer RTL8812CU, ch6):
- Build + ctest 80/80, CI green (23/23).
- Interrupt rc:
mt7612uprobe norsp 6 60 1sent SIGTERM at 8 s printedGATE NORSP: INTERRUPTED - no verdict (restored)and exited 3. - autoack: 3 passed (A 1665/1665 at 0.04, B/C/D 0% at 12.00, E 1668/1668).
- identity: the
iw dev ... type APcheck brought the harness past hostapd with no shim on the Arch host.staid12/12,staackarm C deaf again. Thestatable then hit the rtw88 stall once more (injector 1005 frames in 124 s, to_us 0 from arm B on) and was refused as it should be. So the BSSID table is still not reproduced on this rig, and that remains a rig limitation, not a finding against the PR. - Adapters handed back after every harness, without the peer race.
One thing to look at, not blocking: uplink now reads INCONCLUSIVE on this unit at both FRAMES=60 and FRAMES=200 (rc 2), where the same unit gave 1 passed on cc48be4. The measurement itself is unambiguous — arm A 199/200 acknowledged at 0.0 mean retries, arm B 0/158 at 16.0 — but the gate's txs table comes back one status entry short in nearly every 6 fps arm (a, c, d, e, f, h all 199/200, twice; 59/60 at FRAMES=60), while the high-fps arms (g at 468 fps) settle 200/200. That off-by-one belongs to gate_txs from #452, not to this PR, but this harness inherits it: any entry deficit on a claiming arm is refused outright. Either find the lost entry in the status-FIFO drain, or give the claiming arm a tolerance (entries ≥ sent − 1, say, with the same "neighbours can only inflate a failing arm" argument you already make for the control). Your unit settles 200/200; this one does not, 3 runs out of 4.
What changed
IRadio::SetStationIdentity(own, bssid)/ClearStationIdentity(): avendor-neutral seam for the STATION half of an infrastructure BSS. The
contract lives at the declaration in
src/IRadio.h. It covers:SetAckResponder(bssid);ClearStationIdentity()returns whether the rollback was verified;an ACK (
build_stream_radiotap(mode, false)). It also needs a nonzerotx.retry_limit: the default is 0, which sends each unicast frame once.The not-ported defaults are
falseforSetStationIdentityandtrueforClearStationIdentity.IRadio::StartRxLoopdoc: which thread runs the callback, and the lockrule with its libusb mechanism. A synchronous transfer completes only when
some thread handles libusb events. In the Async RX mode that thread is the
RX thread, which is inside the callback waiting on the caller's lock.
AdapterCaps::station_mode_ok(thestation_modefield of theadapter.capsevent). TRUE on the MT7612U, FALSE (not ported) on everyother backend.
The MT7612U station arm,
src/mt7612u/station.cpp:mt7612u_set_station_identity()writes no register. It refuses unlessownalready isMT_MAC_ADDRandMT_AUTO_RSP_ENis set, and both readsfail closed. It records the BSSID for the host.
The decision is the pure
src/mt7612u/StationIdentity.h.After every write to
MT_MAC_ADDR(ACK responder, beacon, responderclear) the arm is re-checked against what the register holds
(
mt7612u_station_identity_check), tri-state:arm it dropped.
Headless cells in
mt7612u_station_identitycover this.Mt7612uRadio::SetStationIdentitywarns whentx.retry_limitis 0.The new C entry points are
mt7612u_set_station_identity,mt7612u_clear_station_identityandmt7612u_station_bssid;api_linknow counts 39.
Hardware gates and harnesses:
mt7612uprobegainsstaid,sta,staack,norspandbssen. Three harnesses drive them and the existingtxsgate:tests/mt7612u_sta_identity.sh,tests/mt7612u_sta_autoack.shand
tests/mt7612u_sta_uplink.sh, plus the helpertests/sta_unicast_inject.py.tests/mt7612u_sta_uplink.shsets the DUT's retry limit explicitly(
RETRY_LIMIT, default 15) and checks it was read back. It refuses 0.Its header states the runtime: about 9 minutes per arm at FRAMES=200.
The harnesses share
tests/mt7612u_sta_lib.sh, and all run as root:removed first);
DUT_SYSFSthat is not 0e8d:7612;mt76x2uon exit with a VID:PID-guardedauthorizedtoggle.The identity harness takes hostapd's PID from
-P. It accepts an APonly if the adapter:
It verifies that the unicast injector actually injected.
Record:
docs/mt7612u-station-identity.md, plus a section insrc/mt7612u/CLAUDE.md.Why
A station has to receive the AP's unicast addressed to it and auto-ACK it. On
the MT7612U, the obvious way to arm that (
SetAckResponder(bssid)) is the onething that breaks it. The interface needed a separate call whose contract says
so. The backend's behaviour had to be measured, not read off registers.
station_mode_oklets a caller refuse up front instead of starting ahandshake it cannot finish.
What is measured
The source is
docs/mt7612u-station-identity.md. Every cell used one MT7612Uon channel 6, near field, driven by
mt7612uproberather than throughIRadio.RTL8812CU's own CCX
tx.report, retry limit 12.(
MT_AUTO_RSP_ENcleared): 0% at 12.00 retries each.same shape, but it was not single-variable.
MT_MAC_BSSID, and the AP's BSSID in the station's APC slot, do notgate a managed station's receive.
MT_MAC_BSSIDprogrammed WRONG: 5877 unicast frames against 6250 withnothing programmed (re-run: 5909 vs 6478).
acknowledgement: autoack arm E on 6e99f43 was acknowledged 867/867.
The DUT log reads
WRONG BSSID 02:00:00:de:ad:02 in station APC slot 0, BIT(16) SET (high reg 000102ad), other slots empty, MT_MAC_BSSID = own address (verified).AP-side rule (mt76 keys a station's slot on its own address, slot 0
here) and are withdrawn;
DUT was A 6277, B 5857, C 5861, D 5846, E 5852 and F (both WRONG, base
and slot 0) 6003. Every write was verified, BIT(16) was clear, and the
managed filter
0x00015f97was in force;did. It does not show the registers are without effect: B-F sit 4-7%
below A, mostly B-E, which is the unexplained first-arm excess every
run shows, and a few-percent effect would hide in it. One unit, one AP,
one run.
MT_MAC_ADDRunder the managed filter drops reception of theAP's unicast from 103 frames to 0.
MT_TX_STAT_FIFO, the peer's ACK responder armed):only as a floor. The first run used the initvals' retry limit of 15; the
later runs programmed 15 and confirmed it by read-back.
Withdrawn (kept in the doc): the BSSID gate's first run, which had the
monitor filter installed. Also the probe-response auto-ACK method
(
staack's verdict), whose control cannot move against hostapd.What it can't do
Mt7612uRadio::StartRxLoopinstalls the monitor filter unconditionally, so a station driven through
IRadiodoes not run the managed filter the cells measured.MT_MAC_ADDRmakes it deaf" is a managed-filter property.IRadio. The seam writes no register, so themeasured state is the state a successful arm leaves. The
arm-then-measure path is still unexercised, and no in-tree demo calls
SetStationIdentity, so the retry-limit warning is not bench-checkedeither.
duplicate detection and hardware key lookup are untested.
src/stacaller. Frame building, thesupplicant, CCMP and the per-transmitter
DupDetectorbelong to whateverdrives the seam (a later PR).
Follow-ups
SetStationIdentityon Jaguar1/2/3).IRadioover thesrc/stacore, and the condensed station doc.
Maintainer review (josephnef)
Blocking, fixed:
gate_stareported "measured" when interrupted.GATE STA: INTERRUPTED - no verdictand returns 3, asgate_txsandgate_tsfwrapdo. It does this after the verified registerreset, so an interrupted table still leaves the chip clean.
gate_staackhad the same break-into-verdict shape and returns 3 too.tests/mt7612u_sta_identity.shreports a gate's 3 as "INTERRUPTED - noverdict" and never as a pass. It also no longer overwrites a 3 with the
injector's rc 1.
Nits, all taken:
gate_norsp/gate_bsseninterrupted dwell:wait_ticking()'sreturn is kept. After the (verified) restore, an interrupted dwell is
INTERRUPTED - no verdict (restored), rc 3.gate_bssen's singlebadpath: split. A failed register write orread-back is rc 1 (a defect); "BIT(16) would not stay set" is rc 2
(inconclusive). A failed restore is rc 1 in either case.
sta_pid_killnow pollskill -0forup to 10 s after the signal, because a process started inside a command
substitution is not our child, so
waitreturns at once. It returns 1 andsays so if the process is still alive. autoack and uplink re-enumerate the
peer only when it has exited, and otherwise say "peer still running - not
re-enumerating".
STA_PEER_IDset before the peer is opened:sta_peer_recordstillchecks the peer up front, but the hand-back now also requires a marker,
sta_peer_opened(a file inOUT, because the peer starts inside a commandsubstitution), written just before a peer process starts. An early exit
therefore bounces nothing this run did not touch.
sta_fw_link/sta_fw_unlinkin the lib,used by all three harnesses.
$ROOT/firmwareneither exists nor is asymlink, with
ln -snand no-f.FW_DIR.PID starttime(from/proc/PID/statfield 22), and a holder counts as live only when bothmatch. A recycled PID is treated as a stale lock.
Header notes, implemented where cheap:
identity.shjudges the AP up byiw dev <if> inforeportingtype AP, polled for up to 10 s, not byAP-ENABLEDin hostapd's-flog(Arch's 2.11 never writes it). The log is kept and shown if it exists.
driver just loaded, it may have moved; re-read lsusb -t". The header
explains the move (bus 9 -> 10, 3-2.3.3 -> 4-2.3.3).
identity.shheader. High speed and nofull-speed hub are necessary but not sufficient; an rtw88 AP can stall on a
SuperSpeed root port too, the rate check refuses such a table, and a
stalled run is re-run.
Second-unit record (CF-922AC
40:a5:ef:5f:65:51, peer 8812CU, AP T3U8812BU on rtw88 at a SuperSpeed root port, hostapd 2.11, ch6, one run per
arm):
staid12/12.16.0.
staackarm C received nothing;staA 6141 / B 5747 (−6.4%);rtw88_8822bu: failed to get tx report from firmware, injector 12289 frames in 123 s, beacons ~120/arm,to_us0 in D–F), and the harness refused the table.Against it:
Both acknowledged every frame, but that difference is unexplained.
inference: a full-speed hub is not the whole story, and the rtw88 8822bu
TX-report stall happens on a fast port too.
The doc (
docs/mt7612u-station-identity.md, "Second unit, second rig" andthe rewritten stall section) and the
station_mode_oklimits now say allthis.
Verification
Review record:
verified each finding against the tree;
ClearStationIdentitydefault, thestation_mode_okpeer sentence), 2wording;
mt7612uprobe staidnow also checks that arming and clearing write noregister, so it expects 12 passed, 0 failed (was 10).
and all 9 fixed:
src/mt7612u/CLAUDE.mdreduced to pointers;(
rx_teardown(): receiver off, then the EP4 ring;bringup.cpprx_teardowncomment,Mt7612uRadio.cppmt7612u_rx_quiesce);sta_dut_takeverifies that no driver is left bound;OUT(lock);second, which the public header requires (
mt7612u_phy_tick); they donow.
driver: txdemo/rxdemo's libusb open detaches it and the harness only
handed back the DUT. autoack and uplink now record the peer's
idVendor:idProduct:serial and re-enumerate it on exit while the path
still reports that ID (
sta_peer_record/sta_peer_handbackintests/mt7612u_sta_lib.sh). This is harness-only; the gates areunchanged.
OUT: an unsetOUTgets a freshmktemp -d(0700), and agiven one is refused if it is a symlink or not owned by root or the
invoking user;
mt_mac_start()fails,since it sets ENABLE_TX before its WPDMA poll. This is an error path
only; the measured path is unchanged;
VID:PID:serial recorded at take;
PEER_VID:PEER_PID, not be a hub, and not be theDUT's path, or the run refuses to start.
cmake -S . -B build -DDEVOURER_MT7612U=ON -DDEVOURER_REQUIRE_STA_CRYPTO_TESTS=ON && cmake --build build -j4 && ctestmt7612u_usb_ids_vs_mt76andmt7612u_initvals_generated, which skip in every configure here).mt7612u_station_identityand the station cryptocells.
Default configure: 78/78 pass.
mt7612u_station_identityis notregistered there, because it is gated on
DEVOURER_MT7612U.RTL8733B-only subset: 73/73 pass.
MT7612U-only subset: 70/70 pass, and
mt7612u_station_identityisregistered.
make -C src/mt7612u check: passes, "api_link: 39 public entry pointsresolved".
Hardware (root,
mt76x2uunbound, run from the repo root; the scriptslink
./firmwareto/lib/firmware/mediatek):mt7612uprobe staid: the contract on the chip.tests/mt7612u_sta_autoack.sh: needs a Realtek CCX peer.tests/mt7612u_sta_identity.sh: needs a hostapd-capable AP adapter.tests/mt7612u_sta_uplink.sh: needs a Realtek ACK-responder peer.Bench record.
Rig, same for both heads:
40:a5:ef:5a:32:f8;a1873ee:
staid: 10/10.staread 5.9-6.5k unicast per arm,filtr=00015f97;staackarm C "received NOTHING".6e99f43:
staid: 10/10.staidpasses, andstaackarm C "received NOTHING".stagate's bring-up then logged 8 MCU timeouts, and every arm read0-8 frames and no beacons. The gate said INCONCLUSIVE, correctly.
a1873ee run).
Adversarial sides:
staack's verdict is not evidence.b94e119 (gates in their final form), same new rig; peer RTL8812CU at 5-1:
slot 0.
staid12/12;staackarm C received nothing.staverified,to_us: A 6268, B 5871, C 5881, D 5885, E 5877,F 6018.
staack's verdict is not evidence.Confirmed with no manual hand-back on 87aef53, cc48be4 and 091ced5:
after each harness the DUT (1-1) was on
mt76x2uand the peer (5-1) onrtl88x2cu, both with their netdevs.091ced5 (this head, after the maintainer review), same rig:
E 842/842 at 0.09 in the verified station slot 0.
at 16.0.
staid12/12, andstaackarm C received nothing.sta"measured": to_us A 6272, B 5887, C 5889, D 5896, E 5887,F 6029.
injector held 35789 frames in 123 s (about 290/s), so the rate guard
passed.
mt7612uprobe stasent SIGTERM mid-arm printed "GATE STA:INTERRUPTED - no verdict" and exited 3.
hostapd process and no monitor interface left behind.
The uplink mean retries still differ from the second unit's (0.0 here,
1.9 there).
523a324, new rig:
staid10/10;staackarm C deaf as recorded (verdict INCONCLUSIVE bydesign).
sta"measured", with the table above.The two failing identity runs (inferred, not proven).
beacons stopped.
failure
mcu.cppdocuments.same full-speed position, and the second unit's rtw88 AP stalled the same
way on a SuperSpeed root port (see the maintainer review). The hub makes
the stall likely; it is not the whole story.
speed or better, and no full-speed hub) as necessary, not sufficient.
gate_stachanged, and no kernel driver touched the DUT mid-run.🤖 Generated with Claude Code
https://claude.ai/code/session_01VNC8xhn1rNCi5t6uLvE6M3