Skip to content

Commit 90970ca

Browse files
josephnefclaude
andauthored
RTL8814AU: pcapng-fidelity USB diff + replay tooling (#55)
## Summary Adds a binary-fidelity USB diff toolchain to compare kernel `aircrack-ng/88XXau` TX vs devourer's on RTL8814AU, plus three env-gated diagnostic switches in `WiFiDriverTxDemo` and one library change to close a concrete kernel-vs-devourer wire-level divergence. The existing `tools/usbmon_diff.py` (#53, text-format) is bulk-OUT only and cannot represent EP0 control transfers — the carrier for ~5000 init register pokes per chip cold-init. Without those axes, "the wire matches" diff conclusions are not load-bearing. The new tool was the difference between "the gate is somewhere in path-A RF table application" (the previous narrowing, indirect) and a concrete URB-level diff against a real kernel-side capture (this PR, direct). ### Tooling - `tools/usbmon_pcap_diff.py` — pcapng-aware diff (Linux usbmon binary, `LINKTYPE_USB_LINUX_MMAPPED = 220`). Surfaces setup packet, full payload + SHA-256, URB flags, status, timestamps, and IN URBs as first-class records. Modes: default position-aligned diff w/ lookahead resync, `--offload-probe`, `--phase-split`, `--aggregate`. Real-capture caveat handled: modern kernels write `flag_setup=0` (not `' '/0x20` as the old docs say) for valid setup packets — parser decodes setup unconditionally on CTRL submits. - `tools/pcapng_to_urbscript.py` — pcapng → binary URB script emitter. - `tools/usbmon_replay.c` → `build/usbmon_replay` — `USBDEVFS_SUBMITURB` verbatim replay. `--dry-run`, `--disconnect`, capped inter-URB gaps, bulk-OUT default `URB_ZERO_PACKET` matching `RtlUsbAdapter::send_packet`. - Unit + roundtrip tests (synthetic pcaps, no hardware) — `tests/test_usbmon_pcap_diff.py`, `tests/test_urbscript_roundtrip.py`. - `tools/usbmon_diff.py` — banner update pointing forward. ### Diagnostic gates in `WiFiDriverTxDemo` (all OFF by default) - `DEVOURER_USB_SENTINEL=1` — 0xDEAD/0xBEEF writes to `REG_DUMMY (0x04FC)` bracketing init, so `--phase-split` can use sentinels instead of the gap heuristic. - `DEVOURER_DRAIN_BULK_IN=1` — background bulk-IN drainer on EP 0x81 (kernel pre-arms 8×32KB; devourer in TX-only mode never had any IN URBs in flight). With this gate the chip starts pushing 176–390 B C2H/status messages back. Empirically necessary but not sufficient — does NOT alone unblock on-air TX. - `DEVOURER_POLL_INTR_IN=1` — EP 0x85 interrupt-IN poller. Confirmed empirically the chip does NOT push on EP 0x85 during devourer init; upstream's `CONFIG_USB_INTERRUPT_IN_PIPE` codepath is not load-bearing for the TX gate. Kept as a diagnostic. ### Library change `RtlUsbAdapter::ReadEFuseByte`: mirror the kernel's per-byte-read `REG_EFUSE_TEST (0x0034) = 0x0000` (16-bit RD-then-WR), 312 times per init. Removes a known concrete divergence flagged by the new diff. Empirically harmless on its own (does NOT close the TX-on-air gate) but matches upstream wire shape — useful as bisection ground truth. ### Real-capture findings (first watertight kernel-vs-devourer diff on 8814AU) Kernel cold-init capture taken inside `devourer-testrig` VM (host kernel 6.18 cannot build `aircrack-ng/88XXau`); devourer-side on the host with chip then handed back. | Axis | Kernel | Devourer | Δ | |---|---|---|---| | Realtek ctrl writes | 6146 | 5399 | +747 | | Realtek ctrl reads | 2337 | 1651 | +686 | | Bulk-IN URBs (EP 0x81) | 8 × 32 KB | 0 | +8 | | Reg 0x1998 (BB cal loop) | 1029 | 781 | **+248** ← biggest single-reg | | Reg 0x0034 (`REG_EFUSE_TEST`) | 312 | 0 → 312 (after patch) | closed | | Path-A/B/C/D LSSI | 370 / 296 / 296 / 296 | 354 / 282 / 282 / 282 | +14 each | Hypotheses tested & falsified this session: FW-offload of path-A RF table; EP 0x85 interrupt-IN polling; REG_CR missing MAC enables; EFUSE_TEST 0x0034 missing writes; bulk-IN drainage as sufficient cause. Strongest remaining open candidate: register 0x1998 (248-write deficit) at `hal/phydm/rtl8814a/Hal8814_PhyTables.c:3607+` — a BB calibration loop whose phydm conditional evaluates a different branch between kernel and devourer. ## Test plan - [x] `cmake --build build -j` clean - [x] `python3 tests/test_usbmon_pcap_diff.py` — 8 cases pass - [x] `python3 tests/test_urbscript_roundtrip.py` — pcap → urbs → replay --dry-run pass - [x] `WiFiDriverTxDemo` with `DEVOURER_USB_SENTINEL=1` runs init+TX with 2 visible sentinel writes - [x] Real pcapng captured via `tshark -i usbmon4 -s 0` parses cleanly; phase-split lands on sentinel boundary deterministically - [x] Kernel-side cold-init capture inside `devourer-testrig` VM, diff against host devourer-side cold-init produces concrete divergence numbers (table above) - [ ] Followup PR: investigate and address the 0x1998 248-write deficit 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent be3c7cf commit 90970ca

9 files changed

Lines changed: 1764 additions & 5 deletions

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
11
compile_commands.json
22
build
33
.cache
4+
__pycache__/
5+
*.pyc

src/RtlUsbAdapter.cpp

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,19 @@ void RtlUsbAdapter::ReadEFuseByte(uint16_t _offset, uint8_t *pbuf) {
212212
uint8_t readbyte;
213213
uint16_t retry;
214214

215+
/* Match the kernel `88XXau` driver's per-iteration EFUSE_TEST clear.
216+
* Cold-init usbmon diff (2026-05-28, devourer-testrig VM kernel-side
217+
* vs host devourer-side) shows the kernel does an RD-then-WR sequence
218+
* at REG_EFUSE_TEST (0x0034) = 0x0000 (16-bit) BEFORE every EFUSE byte
219+
* read, 312 times per init; devourer never touched 0x0034. We mirror
220+
* the sequence so the EFUSE state machine sees identical wire shape
221+
* across all 312 byte reads. Empirically harmless on its own (does
222+
* NOT fix the RTL8814AU TX-on-air gate per a sniffer run with this
223+
* patch + bulk-IN drainer enabled) but removes a known concrete
224+
* wire-level divergence flagged by tools/usbmon_pcap_diff.py. */
225+
(void)rtw_read16(REG_EFUSE_TEST);
226+
rtw_write16(REG_EFUSE_TEST, 0x0000);
227+
215228
/* Write Address */
216229
rtw_write8(EFUSE_CTRL + 1, (uint8_t)(_offset & 0xff));
217230
readbyte = rtw_read8(EFUSE_CTRL + 2);

tests/test_urbscript_roundtrip.py

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
#!/usr/bin/env python3
2+
"""End-to-end smoke test: pcapng → urbscript emitter → C replay (dry-run).
3+
4+
Synthesises a small pcapng with a mix of control writes, control reads, a
5+
bulk OUT, and an interrupt IN. Runs tools/pcapng_to_urbscript.py to produce
6+
a .urbs file, then runs build/usbmon_replay --dry-run on it and verifies
7+
the emitted submit counts match the script.
8+
9+
Requires build/usbmon_replay to have been compiled
10+
(cc -O2 -Wall -Wextra -o build/usbmon_replay tools/usbmon_replay.c).
11+
"""
12+
13+
from __future__ import annotations
14+
15+
import os
16+
import subprocess
17+
import sys
18+
import tempfile
19+
from pathlib import Path
20+
21+
ROOT = Path(__file__).resolve().parents[1]
22+
sys.path.insert(0, str(ROOT))
23+
24+
from tests.test_usbmon_pcap_diff import ( # noqa: E402
25+
_build_pcap,
26+
_bulk_out,
27+
_ctrl_read,
28+
_ctrl_write,
29+
_interrupt_in,
30+
)
31+
32+
33+
def main() -> int:
34+
replay_bin = ROOT / "build" / "usbmon_replay"
35+
if not replay_bin.exists():
36+
print(f"FAIL: {replay_bin} not built", file=sys.stderr)
37+
print(" cc -O2 -Wall -Wextra -o build/usbmon_replay tools/usbmon_replay.c",
38+
file=sys.stderr)
39+
return 1
40+
41+
with tempfile.TemporaryDirectory() as d:
42+
tmp = Path(d)
43+
pcap = tmp / "in.pcap"
44+
urbs = tmp / "in.urbs"
45+
46+
records = []
47+
# 3 control writes, 1 control read, 1 bulk OUT, 1 interrupt IN.
48+
records += list(_ctrl_write(0x0100, b"\x01", 1_000_000, 1))
49+
records += list(_ctrl_write(0x0102, b"\xab\xcd", 1_000_500, 2))
50+
records += list(_ctrl_write(0x0C90, b"\x55\xaa\x55\xaa", 1_001_000, 3))
51+
records += list(_ctrl_read(0x0F00, b"\x12\x34", 1_001_500, 4))
52+
records += list(_bulk_out(0x02, b"hello world " * 8, 1_002_000, 5))
53+
records += list(_interrupt_in(0x83, b"\xc2\xc2\xc2", 1_002_500, 6))
54+
pcap.write_bytes(_build_pcap(records))
55+
56+
# Stage 1 — pcap → urbscript.
57+
r = subprocess.run(
58+
[sys.executable, str(ROOT / "tools" / "pcapng_to_urbscript.py"),
59+
str(pcap), "-o", str(urbs)],
60+
check=True, capture_output=True, text=True,
61+
)
62+
assert urbs.exists(), "urbscript not emitted"
63+
out = r.stdout + r.stderr
64+
# 6 submits total.
65+
assert "wrote 6 URB records" in out, f"unexpected stage-1 output:\n{out}"
66+
print(f" stage 1 (pcap → urbs): ok")
67+
68+
# Stage 2 — replay --dry-run.
69+
r = subprocess.run(
70+
[str(replay_bin), "--device", "/dev/null",
71+
"--urbs", str(urbs), "--dry-run", "-v"],
72+
check=True, capture_output=True, text=True,
73+
)
74+
out = r.stderr
75+
# Expect 6 submits, 6 ok, by kind: ctrl=4 bulk=1 intr=1, in=2 out=4
76+
assert "6 submits, ok=6" in out, f"summary missing or wrong:\n{out}"
77+
assert "ctrl=4 bulk=1 intr=1" in out, f"by-kind wrong:\n{out}"
78+
assert "in=2 out=4" in out, f"by-dir wrong:\n{out}"
79+
print(f" stage 2 (urbs → replay --dry-run): ok")
80+
print("ALL OK")
81+
return 0
82+
83+
84+
if __name__ == "__main__":
85+
sys.exit(main())

0 commit comments

Comments
 (0)