From 4c1ecc447577de5750ee7fc69b4770766fc83198 Mon Sep 17 00:00:00 2001 From: Gilang Date: Tue, 8 Sep 2026 22:05:41 +0700 Subject: [PATCH 1/3] jaguar3: honour rx.keep_corrupted in the monitor RCR (ACRC32|AICV) DEVOURER_RX_KEEP_CORRUPTED / DeviceConfig::rx.keep_corrupted was a silent no-op on Jaguar3 (RTL8812CU/8822CU, RTL8812EU/8822EU). monitor_rx_cfg wrote a hardcoded RCR of 0xF410400F and its comment claimed that value already accepted CRC/ICV-error frames. Decoding the literal against halmac_bit_8822c.h / halmac_bit_8822e.h shows ACRC32 (BIT8) and AICV (BIT9) are not in it, so the WMAC dropped every FCS-failed frame no matter how the config field was set, and the corrupted-frame retention the fused-FEC salvage layer is built on never worked on this generation. The fix gates the two bits on _cfg.rx.keep_corrupted, which is what Jaguar1, Jaguar2 and the RTL8733B already do. Both misleading comments are corrected, including a full decode of the base literal. The separate `| (1u << 28)` for APP_PHYSTS is dropped: bit 28 is already inside the leading 0xF nibble, and ORing it again read as if it were being added. Default behaviour of the library is unchanged: keep_corrupted defaults to false, so an unconfigured Jaguar3 monitor session programs exactly the RCR it programmed before. One shipped tool does change: `doctor` sets keep_corrupted unconditionally, so on Jaguar3 its printed rx_frames_crc goes from structurally-always-0 to a real count. The HEALTHY/SUSPECT verdict reads rx_frames_ok separately and does not move. Also widens the keep_corrupted doc comment in DeviceConfig.h to name Jaguar3 and the RTL8733B, and to state that Kestrel has no port. Validation (tests/keep_corrupted_j3_ab.sh, one 20-30 s cell per arm, host counting rx.corrupt events with the descriptor's crc_err bit set): 8812CU (0bda:c812) ch6: off 2894 frames / 0 crc_err on 4152 frames / 1258 crc_err (2894 good) 8812EU (0bda:a81a) ch36: off 47351 frames / 0 crc_err on 41726 frames / 1918 crc_err One unit of each die, one bench. The 8812EU unit on this bench hears nothing on 2.4 GHz with or without the knob, so its 2.4 GHz cell is uninformative rather than a counter-example. Co-Authored-By: Claude Fable 5.1 --- src/DeviceConfig.h | 3 ++- src/jaguar3/HalJaguar3.cpp | 24 +++++++++++++++++++----- tests/keep_corrupted_j3_ab.sh | 35 +++++++++++++++++++++++++++++++++++ 3 files changed, 56 insertions(+), 6 deletions(-) create mode 100755 tests/keep_corrupted_j3_ab.sh diff --git a/src/DeviceConfig.h b/src/DeviceConfig.h index d3c10de9..c45fe20e 100644 --- a/src/DeviceConfig.h +++ b/src/DeviceConfig.h @@ -117,7 +117,8 @@ struct DeviceConfig { struct Rx { /* env: DEVOURER_RX_KEEP_CORRUPTED — pass frames that fail the 802.11 FCS * (CRC32) or decryption-ICV check up to the host instead of dropping them - * at the WMAC filter (sets RCR ACRC32|AICV). Jaguar1 + Jaguar2. */ + * at the WMAC filter (sets RCR ACRC32|AICV). Jaguar1, Jaguar2, Jaguar3 and + * the RTL8733B; not ported on Kestrel, where it is silently inert. */ bool keep_corrupted = false; /* env: DEVOURER_TX_WITH_RX — Jaguar3 only: keep the RX filters open and * enable the RX path during a TX (InitWrite) bring-up so StartRxLoop can diff --git a/src/jaguar3/HalJaguar3.cpp b/src/jaguar3/HalJaguar3.cpp index 66abdca6..ba938e18 100644 --- a/src/jaguar3/HalJaguar3.cpp +++ b/src/jaguar3/HalJaguar3.cpp @@ -461,9 +461,13 @@ void HalJaguar3::rtw_hal_deinit() { } /* Monitor-mode RX configuration (devourer-specific; the vendor driver has no - * pure-monitor path). Accept all frames incl. CRC/ICV errors, append PHY status - * drvinfo (so parse_rx_8822c's drvinfo_size is consistent), all RX filter maps - * open. RCR bits: AAP/APM/AM/AB/ACF/AICV/ACRC32 + APP_PHYSTS. */ + * pure-monitor path). Accept all frames, append PHY status drvinfo (so + * parse_rx_8822c's drvinfo_size is consistent), all RX filter maps open. + * RCR base 0xF410400F = AAP/APM/AM/AB + HTC_LOC_CTRL + PKTCTL_DLEN + VHT_DACK + * + APP_FCS/APP_MIC/APP_ICV/APP_PHYSTS (halmac_bit_8822c.h). ACRC32 (BIT8) + * and AICV (BIT9) are NOT in the base — the WMAC drops FCS/ICV-failed frames + * — and are added only under rx.keep_corrupted, same as Jaguar1/2 and the + * RTL8733B. */ void HalJaguar3::monitor_rx_cfg() { constexpr uint16_t REG_RCR_8822C = 0x0608; constexpr uint16_t REG_RXFLTMAP0_8822C = 0x06A0; @@ -474,14 +478,24 @@ void HalJaguar3::monitor_rx_cfg() { * MACRXEN(+ENSWBCN). init_mac_cfg only set CR=0x0F (DMA enable); without * MACRXEN (BIT7) the MAC RX engine never runs — the structured-path RX gap. */ _device.rtw_write16(0x0100, 0x06FF); - /* accept-all + keep FCS/ICV-error frames + append phy-status (BIT28). + /* accept-all + append phy-status (BIT28 is inside the leading 0xF nibble). * BIT0 (AAP) is what makes monitor mode promiscuous for unicast: without it * the WMAC passes only broadcast/multicast/physical-match (APM|AM|AB) up, * silently dropping unicast frames addressed to third parties — e.g. NDPA * control frames and VHT beamforming reports, which is why the beamformee * (whose arm programs the self-MAC to the NDPA RA) saw sounding frames while * a plain monitor did not. */ - _device.rtw_write32(REG_RCR_8822C, 0xF410400F | (1u << 28)); + uint32_t rcr = 0xF410400F; + /* DEVOURER_RX_KEEP_CORRUPTED: also pass FCS/ICV-failed frames (ACRC32 BIT8, + * AICV BIT9). The vendor 8822E driver clears both in init_misc and on every + * opmode change except monitor; the RX descriptor's crc_err/icv_err bits + * (parse_rx_8822c) mark the frames so a FEC consumer can salvage them. + * Verified on one 8812EU unit: with the bits set, FCS-failed frames reach + * the host; without them the count is zero regardless of channel + * conditions. */ + if (_cfg.rx.keep_corrupted) + rcr |= (1u << 8) | (1u << 9); + _device.rtw_write32(REG_RCR_8822C, rcr); _device.rtw_write8(REG_RX_DRVINFO_SZ_8822C, 0x04); _device.rtw_write16(REG_RXFLTMAP0_8822C, 0xFFFF); _device.rtw_write16(REG_RXFLTMAP1_8822C, 0xFFFF); diff --git a/tests/keep_corrupted_j3_ab.sh b/tests/keep_corrupted_j3_ab.sh new file mode 100755 index 00000000..c8678f8b --- /dev/null +++ b/tests/keep_corrupted_j3_ab.sh @@ -0,0 +1,35 @@ +#!/usr/bin/env bash +# A/B for DEVOURER_RX_KEEP_CORRUPTED on a Jaguar3 DUT: run rxdemo twice on the +# same channel (knob off, then on), count frames the host saw with the RX +# descriptor's crc_err bit set. Expected: 0 with the knob off (WMAC drops them), +# non-zero with it on — given any ambient traffic weak enough to fail FCS. +# +# sudo tests/keep_corrupted_j3_ab.sh [channel] [seconds] +set -euo pipefail +PID=${1:?usage: $0 [channel] [seconds]} +CH=${2:-6} +SECS=${3:-20} +ROOT=$(cd "$(dirname "$0")/.." && pwd) +RX=$ROOT/build/rxdemo +OUT=${OUT:-/tmp/keep_corrupted_j3_ab} +mkdir -p "$OUT" +pids=() +cleanup() { for p in "${pids[@]:-}"; do [ -n "$p" ] && kill -INT "$p" 2>/dev/null || true; done; } +trap cleanup EXIT + +run_cell() { + local knob=$1 + local log=$OUT/pid${PID}_keep${knob}.jsonl + local env=(DEVOURER_PID="$PID" DEVOURER_CHANNEL="$CH" DEVOURER_RX_DUMP_ALL=1 DEVOURER_LOG_LEVEL=warn) + [ "$knob" = 1 ] && env+=(DEVOURER_RX_KEEP_CORRUPTED=1) + env "${env[@]}" "$RX" >"$log" 2>"$log.err" & + local p=$!; pids+=("$p") + sleep "$SECS" + kill -INT "$p" 2>/dev/null || true; wait "$p" 2>/dev/null || true + local total crc + total=$(grep -c -F '"ev":"rx.corrupt"' "$log" || true) + crc=$(grep -F '"ev":"rx.corrupt"' "$log" | grep -c -F '"crc":1' || true) + echo "pid=$PID keep_corrupted=$knob ch=$CH secs=$SECS frames=$total crc_err=$crc" +} +run_cell 0 +run_cell 1 From c0744c7a271c735c451f3b8a17740cd7218c097b Mon Sep 17 00:00:00 2001 From: Joseph <162703152+josephnef@users.noreply.github.com> Date: Thu, 17 Sep 2026 16:44:39 +0300 Subject: [PATCH 2/3] tests: keep_corrupted_j3_ab.sh scrubs the knob for the off arm and refuses a dead cell The off arm now runs under env -u DEVOURER_RX_KEEP_CORRUPTED, so a caller who exported the knob cannot silently turn both arms into the on arm. A cell whose rxdemo is no longer running when the dwell ends (device open, claim or bring-up failed) is reported as FAIL with its stderr tail instead of a zero-frame "result". Both re-checked on the 8812CU (off 1962/0, on 2462/647 crc_err in 10 s) and the failure path on a bogus PID. Co-Authored-By: Claude Fable 5.1 --- tests/keep_corrupted_j3_ab.sh | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/tests/keep_corrupted_j3_ab.sh b/tests/keep_corrupted_j3_ab.sh index c8678f8b..4ea386aa 100755 --- a/tests/keep_corrupted_j3_ab.sh +++ b/tests/keep_corrupted_j3_ab.sh @@ -20,11 +20,22 @@ trap cleanup EXIT run_cell() { local knob=$1 local log=$OUT/pid${PID}_keep${knob}.jsonl - local env=(DEVOURER_PID="$PID" DEVOURER_CHANNEL="$CH" DEVOURER_RX_DUMP_ALL=1 DEVOURER_LOG_LEVEL=warn) + # The off arm scrubs the knob from the environment (env -u) rather than + # relying on the caller not having exported it; the on arm sets it. + local env=(-u DEVOURER_RX_KEEP_CORRUPTED + DEVOURER_PID="$PID" DEVOURER_CHANNEL="$CH" DEVOURER_RX_DUMP_ALL=1 DEVOURER_LOG_LEVEL=warn) [ "$knob" = 1 ] && env+=(DEVOURER_RX_KEEP_CORRUPTED=1) env "${env[@]}" "$RX" >"$log" 2>"$log.err" & local p=$!; pids+=("$p") sleep "$SECS" + # A cell whose rxdemo is no longer running did not measure anything (device + # open / claim / bring-up failed) — refuse to print a zero as a result. + if ! kill -0 "$p" 2>/dev/null; then + local rc=0; wait "$p" 2>/dev/null || rc=$? + echo "FAIL: pid=$PID keep_corrupted=$knob rxdemo exited early (rc=$rc); stderr tail:" >&2 + tail -n 5 "$log.err" >&2 + exit 1 + fi kill -INT "$p" 2>/dev/null || true; wait "$p" 2>/dev/null || true local total crc total=$(grep -c -F '"ev":"rx.corrupt"' "$log" || true) From 7352dc033a6f3f5376d0d6dd64114d7e8630eae1 Mon Sep 17 00:00:00 2001 From: Joseph <162703152+josephnef@users.noreply.github.com> Date: Thu, 17 Sep 2026 16:49:05 +0300 Subject: [PATCH 3/3] keep_corrupted: name the MT7612U in the doc comment; A/B script forces events to stdout Mt7612uRadio forwards rx.keep_corrupted into mt7612u_set_monitor_rx, so the doc comment listing where the knob is honoured now includes it, and the mechanism wording no longer assumes a Realtek RCR. The A/B script pins DEVOURER_EVENTS=stdout, the stream it counts from, so an exported DEVOURER_EVENTS=off|stderr cannot turn both arms into zeros. Co-Authored-By: Claude Fable 5.1 --- src/DeviceConfig.h | 5 +++-- tests/keep_corrupted_j3_ab.sh | 6 ++++-- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/DeviceConfig.h b/src/DeviceConfig.h index c45fe20e..83549860 100644 --- a/src/DeviceConfig.h +++ b/src/DeviceConfig.h @@ -117,8 +117,9 @@ struct DeviceConfig { struct Rx { /* env: DEVOURER_RX_KEEP_CORRUPTED — pass frames that fail the 802.11 FCS * (CRC32) or decryption-ICV check up to the host instead of dropping them - * at the WMAC filter (sets RCR ACRC32|AICV). Jaguar1, Jaguar2, Jaguar3 and - * the RTL8733B; not ported on Kestrel, where it is silently inert. */ + * at the MAC RX filter (Realtek: RCR ACRC32|AICV; MT7612U: its monitor + * RX filter). Jaguar1, Jaguar2, Jaguar3, the RTL8733B and the MT7612U; + * not ported on Kestrel, where it is silently inert. */ bool keep_corrupted = false; /* env: DEVOURER_TX_WITH_RX — Jaguar3 only: keep the RX filters open and * enable the RX path during a TX (InitWrite) bring-up so StartRxLoop can diff --git a/tests/keep_corrupted_j3_ab.sh b/tests/keep_corrupted_j3_ab.sh index 4ea386aa..394fb9ff 100755 --- a/tests/keep_corrupted_j3_ab.sh +++ b/tests/keep_corrupted_j3_ab.sh @@ -21,9 +21,11 @@ run_cell() { local knob=$1 local log=$OUT/pid${PID}_keep${knob}.jsonl # The off arm scrubs the knob from the environment (env -u) rather than - # relying on the caller not having exported it; the on arm sets it. + # relying on the caller not having exported it; the on arm sets it. Events + # are forced to stdout because that is the stream the counts come from. local env=(-u DEVOURER_RX_KEEP_CORRUPTED - DEVOURER_PID="$PID" DEVOURER_CHANNEL="$CH" DEVOURER_RX_DUMP_ALL=1 DEVOURER_LOG_LEVEL=warn) + DEVOURER_PID="$PID" DEVOURER_CHANNEL="$CH" DEVOURER_RX_DUMP_ALL=1 + DEVOURER_EVENTS=stdout DEVOURER_LOG_LEVEL=warn) [ "$knob" = 1 ] && env+=(DEVOURER_RX_KEEP_CORRUPTED=1) env "${env[@]}" "$RX" >"$log" 2>"$log.err" & local p=$!; pids+=("$p")