Skip to content

jaguar3: EFUSE walk assumed ordered sections and quit early (8822C read rfe_type=0) - #384

Merged
josephnef merged 3 commits into
OpenIPC:masterfrom
snokvist:fix/efuse-walk-unordered-sections
Aug 7, 2026
Merged

jaguar3: EFUSE walk assumed ordered sections and quit early (8822C read rfe_type=0)#384
josephnef merged 3 commits into
OpenIPC:masterfrom
snokvist:fix/efuse-walk-unordered-sections

Conversation

@snokvist

@snokvist snokvist commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

The bug

HalJaguar3::read_efuse_logical_map stopped walking as soon as a section's
logical base passed the byte the caller asked for:

if (base > upto + 8)
  break; /* past the byte we need */

That is only valid if sections appear in ascending base order. They do not.

Physical EFUSE dumped off an RTL8822CU (0bda:c812), decoded by hand:

phys 0x00  hdr=0x00         -> base 0x000   ok
phys 0x09  hdr=0x10         -> base 0x008   ok
phys 0x12  hdr=0x0F ext=48  -> base 0x100   <- early exit fires here
phys 0x2C  hdr=0x4F ext=5D  -> base 0x150   never reached
phys 0xD5  hdr=0x4F ext=4E  -> base 0x110   never reached
phys 0xDA  hdr=0x4F ext=5E  -> base 0x150   never reached

The third section on the chip jumps to base 0x100, so any request below that
— including EEPROM_RFE_OPTION_8822C at logical 0xCA, which is the whole reason
read_efuse_rfe_type() calls this — ended the walk after three sections and
returned a map that was 0xFF almost everywhere.

Why it matters

On the affected adapter read_efuse_rfe_type() returned 0, while the vendor
kernel driver reads 0x03 from the same chip
(/proc/net/rtl88x2cu/<iface>/efuse_map, logical 0xCA). The RFE type gates BB /
RFE configuration, so those units were being brought up against an unprogrammed
default rather than their actual front-end.

It is silent: nothing errors, the map just reads unprogrammed.

The fix

Walk the whole programmed area (the existing 0xFF-header terminator and
kPhysMax bound already stop it). The upto parameter is removed rather than
left unused — a parameter that still looks like it bounds the walk is how this
comes back.

The 8822E branch is untouched: it never used upto, terminating on a long 0xFF
run instead, which is why only the C path was affected.

Hardware verification

adapter before after kernel (efuse_map 0xCA)
RTL8822CU (0bda:c812, C8822C) rfe_type=0x00 rfe_type=0x03 0x03
RTL8822EU (0bda:a81a, C8822E) rfe_type=0x15 rfe_type=0x15 0x15

The EU is the regression check — unchanged, and its efuse decoded (0x22=46 0x4c=51 0xca=15) line is identical before and after. The 8822C EFUSE stability
probe also now reports a valid 0x8129 EEPROM ID.

Found while implementing #383 (EFUSE MAC as a per-unit identity), which could not
read the MAC on 8822C for this reason. With this fix that adapter's MAC decodes
correctly — 40:a5:ef:2f:23:08, matching its netdev exactly. The two changes are
independent; this one stands on its own regardless of what happens to #383.

@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Aug 6, 2026

Copy link
Copy Markdown

PR Summary by Qodo

Jaguar3: fix EFUSE logical-map walk for unordered sections (8822C RFE type)

🐞 Bug fix 📝 Documentation 🕐 20-40 Minutes

Grey Divider

AI Description

• Decode the full programmed EFUSE area to handle sections out of logical-base order.
• Remove the misleading upto parameter and update all call sites to the new API.
• Guard physical reads past kPhysMax to avoid 10-bit address wraparound aliasing.
Diagram

graph TD
  A["read_efuse_rfe_type"] --> D["read_efuse_logical_map"] --> E["bounded rd()"] --> F[("Physical EFUSE")]
  B["read_efuse_txpwr_base_8822e"] --> D --> E --> F
  C["probe_efuse_map / cache_efuse_8822e"] --> D --> E --> F
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Two-pass scan to enable safe early-exit
  • ➕ Could reduce work if callers only need a small logical range
  • ➕ Makes early termination explicit (based on complete section inventory)
  • ➖ More complex (must first parse headers/lengths, then selectively decode)
  • ➖ EFUSE size is small (1KB), so savings are typically negligible
  • ➖ Higher risk of reintroducing subtle termination bugs
2. Build and reuse a per-section index/cache
  • ➕ Amortizes decode cost across multiple reads
  • ➕ Can support selective logical reads once indexed
  • ➖ Adds cache invalidation/lifecycle complexity
  • ➖ Not needed unless EFUSE decode is a proven hotspot
  • ➖ Increases statefulness in a correctness-critical path

Recommendation: Keep the PR’s approach: always decode the whole programmed EFUSE area and remove the misleading upto parameter. Given unordered section bases and the small physical EFUSE size (1KB), the full-walk strategy is the simplest and most robust way to avoid silent under-decoding; the added kPhysMax guard further hardens correctness against address wraparound.

Files changed (2) +40 / -16

Bug fix (1) +34 / -13
HalJaguar3.cppFix EFUSE walk termination; add kPhysMax guard; update call sites +34/-13

Fix EFUSE walk termination; add kPhysMax guard; update call sites

• Removes the 'upto'-based early-break from the 8822C EFUSE packed-map walk so decoding doesn’t assume sections appear in ascending logical-base order. Adds a physical-address bounds guard in the byte reader to prevent 10-bit address masking from wrapping reads past 'kPhysMax' back to 0. Updates all internal callers to use the new 'read_efuse_logical_map(map, len)' signature and relocates/updates the explanatory comment onto the function.

src/jaguar3/HalJaguar3.cpp

Documentation (1) +6 / -3
HalJaguar3.hUpdate EFUSE decode API and document unordered-section invariant +6/-3

Update EFUSE decode API and document unordered-section invariant

• Updates the private 'read_efuse_logical_map' declaration to drop the 'upto' parameter and documents that the EFUSE walk must cover the whole programmed area because section bases are not ordered. Keeps the interface aligned with the corrected implementation in the .cpp.

src/jaguar3/HalJaguar3.h

@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Aug 6, 2026

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (1) 📘 Rule violations (0) 📎 Requirement gaps (0) 🎨 UX issues (0) 🔗 Cross-repo conflicts (0) 📜 Skill insights (0)

Grey Divider


Action required

1. Missing lambda capture 🐞 Bug ≡ Correctness
Description
read_efuse_logical_map()’s rd lambda references the function-local kPhysMax but does not
capture it ([this, eu]), which is ill-formed in standard C++ and can break the build. This was
introduced by the new if (a >= kPhysMax) guard inside the lambda.
Code

src/jaguar3/HalJaguar3.cpp[R655-656]

+    if (a >= kPhysMax)
+      return 0xFF;
Evidence
The code defines kPhysMax in function scope and then uses it inside a lambda that does not capture
it; this is a direct, local inconsistency that can prevent compilation of this translation unit.

src/jaguar3/HalJaguar3.cpp[635-661]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`HalJaguar3::read_efuse_logical_map()` defines `kPhysMax` as a function-local `constexpr` and then uses it inside the `rd` lambda (`if (a >= kPhysMax)`), but the lambda capture list is `[this, eu]`. In standard C++, referencing an automatic local variable inside a lambda requires capturing it, so this can fail compilation.
### Issue Context
This guard was added to prevent `efuse_OneByteRead` address-masking aliasing when reads go past the physical EFUSE bound.
### Fix Focus Areas
- src/jaguar3/HalJaguar3.cpp[635-661]
### Suggested fix
Update the lambda capture list to include `kPhysMax` (e.g. `auto rd = [this, eu, kPhysMax](uint16_t a) -> uint8_t { ... }`), or move `kPhysMax` to a namespace/class-scope constant so it’s not a function-local capture concern.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. EFUSE read past kPhysMax ✓ Resolved 🐞 Bug ☼ Reliability
Description
After removing the early-exit, read_efuse_logical_map can now reach the end of the physical EFUSE
and still attempt additional rd(phys++) reads for ext headers / data words without checking `phys
< kPhysMax. Because RtlAdapter::efuse_OneByteRead` masks the address to 10 bits, an out-of-range
read (>=1024) aliases to address 0, silently corrupting the decoded logical map.
Code

src/jaguar3/HalJaguar3.cpp[L707-708]

-    if (base > upto + 8)
-      break; /* past the byte we need */
Evidence
The EFUSE walk reads headers, optional ext headers, and data bytes by incrementing phys without
checking against kPhysMax after the loop-head condition, and efuse_OneByteRead explicitly masks
the address high bits, so reads beyond 1023 alias to low addresses instead of failing safely.

src/jaguar3/HalJaguar3.cpp[695-723]
src/jaguar3/HalJaguar3.cpp[649-655]
src/RtlAdapter.cpp[170-200]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`HalJaguar3::read_efuse_logical_map` reads physical EFUSE bytes using `rd(phys++)` but does not guard against `phys` advancing beyond `kPhysMax` within a section (ext header + enabled data words). With this PR removing the early-exit, the function is more likely to walk to the physical end and hit this case.
On the non-EU path, `rd()` uses `RtlAdapter::efuse_OneByteRead`, which masks the address down to 10 bits. If `phys` reaches 1024, the effective address becomes 0 and the decode silently starts reading the beginning of EFUSE again.
## Issue Context
The outer loop condition only checks `phys < kPhysMax` at the top of the loop; it does not prevent `rd(phys++)` inside the loop from executing with `phys == kPhysMax`.
## Fix Focus Areas
- src/jaguar3/HalJaguar3.cpp[637-724]
- src/RtlAdapter.cpp[170-201]
### Suggested implementation direction
- Make `rd(a)` explicitly return `0xFF` without touching hardware when `a >= kPhysMax`.
- Additionally (or alternatively), add `if (phys >= kPhysMax) break;` guards before every `rd(phys++)` that occurs after the header read (ext header + each data byte), so a truncated final section can’t trigger an out-of-range read.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

3. Stale upto documentation ✓ Resolved 🐞 Bug ⚙ Maintainability
Description
The comment above read_efuse_logical_map in HalJaguar3.h still describes an upto parameter even
though the method signature no longer takes it, which is misleading for maintainers and future
callers.
Code

src/jaguar3/HalJaguar3.h[R198-199]

+   * holding) offset `upto`. Backs read_efuse_rfe_type + read_efuse_txpwr_base.
+   * Walks the whole programmed area: sections are NOT ordered by logical base,
Evidence
The declaration has only (uint8_t *map, size_t len) while the comment still says it decodes up to
offset upto.

src/jaguar3/HalJaguar3.h[197-201]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The header comment still references an `upto` parameter that no longer exists in the method signature.
## Issue Context
The implementation and call sites were updated to remove `upto`, but the declaration’s comment was only partially updated.
## Fix Focus Areas
- src/jaguar3/HalJaguar3.h[197-201]
### Suggested implementation direction
- Remove the mention of `upto` from the comment and describe the new behavior (walks the whole programmed area; no early-exit due to unordered sections).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


To customize comments, go to the Qodo configuration screen, or learn more in the docs.

Qodo Logo

Comment thread src/jaguar3/HalJaguar3.h Outdated
Comment thread src/jaguar3/HalJaguar3.cpp
@snokvist

snokvist commented Aug 6, 2026

Copy link
Copy Markdown
Contributor Author

Both review findings addressed.

1. Read past kPhysMax — real, and this PR did make it reachable. The loop head checks phys < kPhysMax once per section, but a header + ext header + four enabled data words advance it up to ten bytes further, and efuse_OneByteRead masks the address to 10 bits — so a read at 1024 aliases to 0 and would decode the start of the EFUSE into whatever logical base the truncated section named. Guarded in the shared rd lambda, which covers both the 8822C and 8822E walks in one place:

if (a >= kPhysMax)
  return 0xFF;

0xFF is already what both walks treat as end-of-map/skip, so the truncated section terminates the walk exactly as an unprogrammed area does.

2. Stale upto in the header comment — my error; I appended to the comment instead of rewriting its first sentence. Reworded to describe the actual behaviour.

Re-verified on hardware after both changes, unchanged from the original results:

adapter rfe_type kernel efuse_map 0xCA
RTL8822CU (C8822C) 0x03 0x03
RTL8822EU (C8822E) 0x15 0x15

@josephnef josephnef left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed with the branch checked out; decode math verified against the dump, callers audited. The fix is correct and the hardware evidence is exactly the right shape — before/after on the affected 8822CU cross-validated against the vendor kernel efuse_map, with the 8822EU as an explicit no-change regression check. CI fully green.

Correctness — verified:

  • The decode math checks out against the dump: hdr=0x0F ext=0x48 → offset 0x20 → base 0x100; hdr=0x4F ext=0x5D0x2A → base 0x150. Both match the hand-decode, so the unordered-sections claim is substantiated, not inferred.
  • Removing upto instead of leaving it unused is the right call — a parameter that looks like a bound but isn't would reintroduce exactly this bug class.
  • Smaller callers stay safe: read_efuse_rfe_type's 0x140-byte stack map vs the section at base 0x150 is handled by the idx < len guard on every data write.
  • The kPhysMax guard in rd() is load-bearing, not belt-and-braces: efuse_OneByteRead masks to 10 bits, so a truncated straddling section would alias back to phys 0 and decode the start of the EFUSE into a bogus logical base. Previously the upto break made that unreachable; with the full walk it matters.
  • 8822E untouched as claimed: the EU branch returns before the removed break, and neither of its callers depended on upto.

Performance: the 8822C non-cached paths now walk the full programmed area (per-byte USB control reads), but the walk still stops at the first 0xFF header and both callers are one-shot at bring-up/probe time — no meaningful cost.

One factual error in the shipped comment (inline, worth fixing before merge — it's the kind of number a future debugging session will trust) plus two comment-style nits inline.

Comment thread src/jaguar3/HalJaguar3.cpp Outdated
Comment thread src/jaguar3/HalJaguar3.cpp Outdated
Comment thread src/jaguar3/HalJaguar3.cpp Outdated
- The comment said the kernel reads 0x15 from the affected 8822CU. 0x15 is
  the 8822EU's value (the regression-check adapter); the 8822CU reads 0x03,
  as both the PR's hardware table and the fix's before/after show.
- The block documents read_efuse_logical_map but sat above probe_efuse_map
  (pre-existing); moved onto the function it describes.
- Present-tense: the invariant is that sections are not in ascending base
  order, with the measured phys dump as its evidence. Git carries the
  "it used to" part.

Comment-only. Build clean, 49/49 ctest.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@snokvist

snokvist commented Aug 6, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for the review — all three comments are addressed in 38790b1 (comment-only; build clean, 49/49 ctest):

  • the 0x15 slip → now 0x03, the affected 8822CU's value. You were right that it is the kind of number a later session trusts; 0x15 was the 8822EU regression-check value in the wrong sentence. The same slip was in the PR body of Expose the EFUSE MAC as a per-unit adapter identity #383 and is fixed there too.
  • the splice + placement → the block no longer tries to say "up to … every offset"; it just says it decodes the whole programmed area, and it moved down onto read_efuse_logical_map. probe_efuse_map keeps its own 8822C-only note.
  • present tense → the invariant with the measured dump as evidence; the "it used to" framing is gone.

Independent cross-check against the vendor parser

Since the argument for removing upto is "sections are not ordered", it is worth having the vendor's own walk on record. eeprom_parser_88xx() (rtl88x2cu 20230728, hal/halmac/halmac_88xx/halmac_efuse_88xx.c:1139) is a do { … } while (1) with no logical-offset bound at all — it terminates only on hdr == 0xff (or hdr2 == 0xff) and on physical-size guards. So the post-fix walk is the vendor's, and the pre-fix upto exit was devourer-local.

Two things fall out of that:

  • The kPhysMax guard is the right shape — the vendor bounds-checks efuse_idx after every advance (header, ext, and each of the 8 data bytes), and is stricter still: it fails the whole parse rather than continuing. Returning 0xFF from rd() is the gentler equivalent.
  • The header decode matches bit-for-bit: vendor blk_idx = ((hdr2 & 0xF0) >> 1) | ((hdr >> 5) & 0x07) vs devourer ((ext & 0xF0) >> 1) | ((hdr & 0xE0) >> 5), so the hand-decode in the comment (0x0F/0x48 → 0x100, 0x4F/0x5D → 0x150) is the vendor's arithmetic, not a re-derivation.

It also answers the "previously-read bytes moving" half of #385: the vendor writes log_map[eeprom_idx] = value8 unconditionally as it walks, so a later section overriding an earlier one is the designed re-burn semantics — and the measured unit does carry two sections at base 0x150 (phys 0x2C and 0xDA).

#385's validation asks

"check that read_efuse_txpwr_base output changes are the intended ones" — there is nothing to check, and that is verifiable rather than hopeful. read_efuse_txpwr_base_8822e() returns early unless _variant == C8822E, and on 8822E the walk takes the EU branch, which returns before the removed exit ever ran. So that reader's output is bit-identical on both variants. The complete consumer set of the changed (non-EU) walk is read_efuse_rfe_type, probe_efuse_map, and #383's MAC read. probe_efuse_map's compare surface does grow — it now sees the whole map instead of the first three sections — which strictly improves what the stability probe is checking.

The RFE blast radius, measured offline. I walked all five 8822C tables twice through PhyTableLoaderJaguar3::Load with rfe_type = 0 and = 3 and diffed the register-write streams:

table writes rfe0 / rfe3 distinct addrs changed added/removed
phy_reg 1289 / 1289 923 / 923 0 0
agc_tab 450 / 450 2 / 2 0 0
radioa 789 / 789 55 / 55 5 0
radiob 697 / 697 37 / 37 2 0
cal_init 2464 / 2464 44 / 44 0 0

Identical counts and identical address sets everywhere — no row appears or disappears. The entire delta is 7 RF register values:

radioa  0x52: 0x000942ca -> 0x000902ca
        0x63: 0x00000c02 -> 0x00000002
        0xb3: 0x0007c760 -> 0x000fc760
        0xb6: 0x000187f8 -> 0x000387f8
        0xdd: 0x00000500 -> 0x00000540
radiob  0x52: 0x000942c0 -> 0x000902ca
        0x63: 0x00000c02 -> 0x00000002

Cut-independent (identical for cut_version 0–3). BB, AGC and RFK cal-init are untouched, so the on-air surface is RF front-end path config only — which is exactly what an RFE type is supposed to select. Happy to land the harness as a ctest if you want it as a standing check; it needs no hardware.

On-air A/B — I don't have an SDR here, so I can't produce the per-rate measurement you asked for. What I can run on the affected 8822CU is a link-level A/B: fixed TX power and channel, N frames per MCS, before/after, comparing per-MCS PDR and receiver RSSI. Say the word and I'll post it. Worth noting the direction of travel independently of any measurement: 0x03 is what the vendor driver programs on that same unit, so the fix moves devourer's RF config onto the kernel's rather than away from it — today those units run the rfe_type=0 column by accident.

Interaction with #383

They conflict, in both directions — GitHub reports each as mergeable against master because it only tests them pairwise with the base. #383 calls read_efuse_logical_map(map, sizeof(map), kMacLogicalOff + 6) in two places, and this PR removes that parameter, so whichever lands second needs the arg dropped at both call sites (cache_efuse_8822e and perm_mac) — a 2-token change, nothing semantic: with upto gone the 8822C on-demand walk simply becomes the full walk it wanted.

I merged both onto master locally and resolved it that way: build clean, 49/49. Merge them in whatever order suits you and I'll push the fixup to the trailing one immediately — or say the word and I'll rebase #383 onto this branch so it lands conflict-free.

@josephnef josephnef left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approving with the on-air arm #385 asked for, run on this rig's affected RTL8822CU (the second of the 2/2 units that reproduce the truncated walk).

On-air TX A/B (master vs this fix, txdemo MCS7/20 flood, SDR duty)

band master (rfe_type=0) fixed (rfe_type=0x03)
ch36 (5 GHz) 49.2% duty (~32.0 Mbps on-air) 48.7% (~31.7 Mbps) — tie
ch6 (2.4 GHz) 78.2–81.8% over 4 interleaved reps 92.5–94.2%

The 2.4 GHz gap is reproducible (interleaved A/B ×3, per-arm sd < 1 point, far above this rig's ±3-point single-probe floor): the correct front-end rows are worth ~+12 points of duty at MCS7/ch6 on this unit — master was flying the unprogrammed-default tables. 5 GHz is unchanged.

RX + identity regression checks

  • RX smoke A/B on the same unit, ch6 ambient: 345 vs 408 clean frames / 8 s, 0 corrupt both arms, HEALTHY verdicts — the table change does not deafen RX.
  • 8822CU efuse: stability probe id 0x8129 valid, rfe_type=0x03 (kernel agrees), and the #383 MAC decodes to a8:b5:8e:6a:94:ea — the exact value the #385 worktree experiment predicted on this unit.
  • 8822EU (98:03:cf:cf:a4:49), 8814AU (20:0d:b0:c7:e4:b3): unchanged, stable across runs.

The kPhysMax guard against the 10-bit address aliasing is a good catch — a truncated tail section would otherwise silently decode the start of the EFUSE into a wrong logical base.

CI note: the pushes landed during today's GitHub Actions incident and the pull_request events were dropped (no runs existed for either head SHA); both PRs were close/reopened to re-trigger. Merge order: this one first, then #383 rides clean.

Comment thread src/jaguar3/HalJaguar3.cpp
@qodo-free-for-open-source-projects

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit 38790b1

@josephnef
josephnef merged commit 1c8ae57 into OpenIPC:master Aug 7, 2026
23 of 25 checks passed
josephnef added a commit that referenced this pull request Aug 7, 2026
## The problem

A consumer that keeps **per-adapter state** — a measured TX-power curve,
a
calibration, anything tied to one specific dongle — has to answer *"is
this the
same physical adapter I measured last time?"* across a re-plug, a
reboot, and a
port change. Applying one unit's measurements to another silently is the
failure
to avoid.

devourer currently exposes no key that can answer it:

- **USB bus path** (`1-1`, `3-1.2`) identifies a *port*, not a device.
It changes
  whenever the dongle moves, orphaning the state.
- **USB serial descriptor** is not unique — see below.

## The serial is a burned-in constant; the MAC beside it is per-unit

Both live in the same EFUSE, a few bytes apart. Dumped through the
vendor kernel
driver (`/proc/net/rtl88x2{eu,cu}/<iface>/efuse_map`) on two adapters:

RTL8822EU — netdev `wlx84fc1450bcde`:
```
0x150   DA 0B 1A A8 FF 7E 02 84   FC 14 50 BC DE 09 03 52
0x170   20 4E 49 43 08 03 31 32   33 34 35 36 FF FF FF FF
```
RTL8822CU — netdev `wlx40a5ef2f2308`:
```
0x150   DA 0B 12 C8 FF 7E 02 40   A5 EF 2F 23 08 09 03 52
0x170   20 4E 49 43 08 03 31 32   33 34 35 36 FF FF FF FF
```

| logical offset | contents | per-unit? |
|---|---|---|
| **0x157** | the 6-byte MAC | **yes** |
| 0x15D / 0x166 | `"Realtek"` / `"802.11ac NIC"` string descriptors | no
|
| 0x174 | USB **serial** string descriptor — `"123456"` | **no,
identical on both** |

So the serial is not *missing*, it is a placeholder burned identically
into every
unit. Keying on it would be worse than the bus path: two adapters in one
host
would share state and silently apply each other's measurements.

The MAC is also exactly where Linux gets it — the vendor driver programs
it into
the netdev, and udev derives the stable `wlx<mac>` name from that. That
name is
stable across re-plug *because* it comes from the chip.

## What this adds

`IRtlDevice::GetPermanentMacAddress(uint8_t out[6])`, defaulting to
`false` so
unimplemented chips degrade gracefully and no existing consumer changes
behaviour.

- **Jaguar1** — routes to the existing `EepromManager::GetMacAddress`.
The read,
the per-chip offsets (from `hal_pg.h`: 8812AU `0xD7`, 8814AU `0xD8`,
8821AU
`0x107`) and the unprogrammed-value rejection were all already
implemented;
  only a route to a caller was missing.
- **Jaguar3** — logical `0x157`. On **8822E** the value is captured
during the
existing `rtw_hal_init` efuse pass, because that OTP is not reliably
readable
after TX/coex bring-up — the same constraint `_efuse_cache` exists for.
One
  walk decodes far enough for both, and `_efuse_cache` keeps its size so
`probe_efuse_map`'s compare surface is unchanged. On **8822C** the map
is
  decoded on demand.
- **doctor** prints it, which is also how to check the offset on a chip
nobody
has measured: compare against the `wlx<mac>` name the vendor driver
gives the
  same dongle.

Jaguar2 and Kestrel keep the default `false`. Kestrel's offset constant
already
exists in-tree (`EFUSE_USB_MAC_ADDR_8852B = 0x488`,
`kestrel/MacRegAx.h:179`) if
someone with the hardware wants to finish it.

## Hardware verification

Three adapters, all three implemented code paths, `doctor` output vs the
kernel's
own netdev MAC:

| adapter | path exercised | kernel | devourer | |
|---|---|---|---|---|
| RTL8812AU (Jaguar1) | `EepromManager`, offset 0xD7 |
`20:0d:b0:c4:a7:6a` | `20:0d:b0:c4:a7:6a` | **match** |
| RTL8822EU (Jaguar3, C8822E) | captured in `rtw_hal_init` |
`84:fc:14:50:bc:de` | `84:fc:14:50:bc:de` | **match** |
| RTL8822CU (Jaguar3, C8822C) | on-demand decode | `40:a5:ef:2f:23:08` |
`40:a5:ef:2f:23:08` | **match** (needs #384) |

**The 8822C row needed a separate fix, and it is not in this PR.** On
that unit the
shared non-EU walk in `read_efuse_logical_map` returned an empty logical
map, so
the MAC read found `0xFF` — independently visible in the same run as
`Jaguar3: rfe_type=0x00` while the kernel reads logical `0xCA = 0x03`
from the same
adapter. Root cause is now known and fixed in **#384** (tracked as
**#385**): the
walk stopped as soon as a section's base passed the requested byte,
which assumes
sections are burned in ascending base order — they are append-ordered.
My original
guess in this PR (the `if (hdr == 0xFF) break;` padding termination) was
**wrong**;
@josephnef's control experiment on a second 8822CU showed EU-style
0xFF-run
tolerance alone changes nothing.

The walk fix stays out of this PR deliberately: it feeds RFE and
per-channel
TX-power base for every 8822C user and deserves its own validation
rather than
riding along with a new accessor. The two changes are independent — this
one has no
unverified path once #384 lands.

## Caveats worth stating

- ~~`0x157` is **measured**, not read from a datasheet.~~ **Resolved** —
it is the
vendor constant. `include/hal_pg.h` (rtl88x2cu 20230728 / rtl88x2eu
20230815):
`EEPROM_MAC_ADDR_8822CU 0x157`, `EEPROM_MAC_ADDR_8822EU 0x157`. The
caveat's worry
was right in kind — the other variants do differ (`…CS/ES 0x16A`,
`…CE/EE 0x120`)
— but devourer is USB-only, so `0x157` is correct for both Jaguar3
parts. The
Jaguar1 offsets served by `EepromManager` check out against the same
header
(8812AU `0xD7`, 8814AU `0xD8`, 8821AU `0x107`), and the Jaguar2
follow-up already
  has its constant there too (8822BU and 8821CU are both `0x107`).
- A MAC is a hardware identity rather than a secret, but it is a stable
device
identifier; consumers logging it should treat it as they treat any other
  adapter identity.

Build is clean; no new warnings.

---------

Co-authored-by: Joseph <162703152+josephnef@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants