feat: wi-fi - join WPA2/WPA3-PSK networks and carry traffic like ethernet

This commit is contained in:
2026-08-06 19:44:35 +02:00
parent a01e63c717
commit bbe1df62fd
40 changed files with 5878 additions and 272 deletions
+367 -12
View File
@@ -17,6 +17,7 @@
*/
#include "Iwx.hpp"
#include "Ieee80211.hpp"
#include <Pci/Pci.hpp>
#include <Memory/HHDM.hpp>
#include <Memory/Paging.hpp>
@@ -459,11 +460,15 @@ namespace Drivers::Net::Wifi {
asm volatile("" ::: "memory");
}
static bool IwxAllocTxRing(IwxTxRing& ring, int qid) {
// `stageSlots` reserves one page per concurrently queued frame; pass 0 for
// queues that only ever carry host commands.
static bool IwxAllocTxRing(IwxTxRing& ring, int qid, uint32_t stageSlots = 0) {
ring.Qid = qid;
ring.Cur = 0;
ring.CurHw = 0;
ring.Queued = 0;
ring.StageSlots = 0;
ring.Active = false;
if (!IwxDmaAlloc(ring.Desc, sizeof(IwxTfhTfd) * IWX_TX_RING_COUNT))
return false;
@@ -476,6 +481,15 @@ namespace Drivers::Net::Wifi {
// are staged in this page instead of the per-slot command area.
if (!IwxDmaAlloc(ring.Bounce, 4096))
return false;
if (stageSlots > IWX_TX_STAGE_SLOTS) stageSlots = IWX_TX_STAGE_SLOTS;
for (uint32_t i = 0; i < stageSlots; i++) {
void* p = Memory::g_pfa->AllocateZeroed();
if (!p) return false;
ring.Stage[i] = (uint8_t*)p;
ring.StagePhys[i] = Memory::SubHHDM(p);
ring.StageSlots = i + 1;
}
return true;
}
@@ -484,6 +498,14 @@ namespace Drivers::Net::Wifi {
IwxDmaFree(ring.BcTbl);
IwxDmaFree(ring.Cmd);
IwxDmaFree(ring.Bounce);
for (uint32_t i = 0; i < ring.StageSlots; i++) {
if (ring.Stage[i]) {
Memory::g_pfa->Free(ring.Stage[i]);
ring.Stage[i] = nullptr;
}
}
ring.StageSlots = 0;
ring.Active = false;
}
static void IwxResetTxRing(IwxTxRing& ring) {
@@ -846,6 +868,9 @@ namespace Drivers::Net::Wifi {
return -1;
}
void IwxDumpFwError();
static uint32_t g_cmdTimeouts = 0; // consecutive unanswered commands
bool IwxSendCmd(IwxHostCmd& hcmd) {
if (g_iwx.State == IwxFwState::Error) return false;
@@ -909,6 +934,12 @@ namespace Drivers::Net::Wifi {
g_iwx.CmdDone = false;
g_iwx.LastCmdId = code;
// Keep the payload so a firmware assert can show exactly what it
// choked on -- struct mismatches are invisible without the bytes.
g_iwx.LastCmdLen = hcmd.Len;
uint32_t keep = hcmd.Len < sizeof(g_iwx.LastCmdPayload)
? hcmd.Len : (uint32_t)sizeof(g_iwx.LastCmdPayload);
if (hcmd.Data && keep) memcpy(g_iwx.LastCmdPayload, hcmd.Data, keep);
g_iwx.CmdWantResp = hcmd.WantResp;
g_iwx.CmdRespLen = 0;
g_iwx.CmdIdx = idx;
@@ -921,18 +952,47 @@ namespace Drivers::Net::Wifi {
// Wait for the firmware's response/ack. Commands are serialized by
// CmdLock, so exactly one can be in flight and the completion is
// unambiguous.
// Two independent bounds. The wall clock is the intended one, but it
// is driven by the timer interrupt, so anything that leaves this loop
// running with interrupts disabled would spin forever and take the
// whole machine down with it -- the spin cap makes that impossible.
// Bail immediately if the firmware has asserted, because it will never
// answer this or any later command.
constexpr uint32_t MAX_SPINS = 20000; // ~2 s at 100 us
bool ok = false;
bool died = false;
uint64_t start = Timekeeping::GetMilliseconds();
while (Timekeeping::GetMilliseconds() - start < 1000) {
for (uint32_t spins = 0; spins < MAX_SPINS; spins++) {
IwxProcessEvents();
if (g_iwx.CmdDone) { ok = true; break; }
if (g_iwx.State == IwxFwState::Error) { died = true; break; }
if (Timekeeping::GetMilliseconds() - start >= 1000) break;
IwxDelayUs(100);
}
if (!ok) {
KernelLogStream(WARNING, "WiFi") << "Command 0x" << base::hex
<< (uint64_t)code << base::dec << " timed out";
<< (uint64_t)code << base::dec
<< (died ? " abandoned: firmware has stopped responding"
: " timed out");
if (ring.Queued > 0) ring.Queued--;
if (!died) {
// Dump on the first silence: the firmware's error table names
// the command that asserted, and it is overwritten as later
// commands go unanswered.
if (++g_cmdTimeouts == 1) IwxDumpFwError();
// Repeated silence means it is wedged and every later command
// would burn the same timeout, so stop trying. A single late
// response is not worth disabling the adapter over.
if (g_cmdTimeouts >= 3) {
KernelLogStream(ERROR, "WiFi")
<< "Firmware stopped responding to host commands";
g_iwx.FwErrors++;
g_iwx.State = IwxFwState::Error;
}
}
} else {
g_cmdTimeouts = 0;
}
g_iwx.CmdWantResp = false;
@@ -969,10 +1029,10 @@ namespace Drivers::Net::Wifi {
// TX queue configuration (used by the connect path)
// =========================================================================
bool IwxEnableTxq(int staId, int qid, int tid) {
IwxTxRing& ring = g_iwx.MgmtQ;
bool IwxEnableTxq(IwxTxRing& ring, int staId, int qid, int tid) {
IwxResetTxRing(ring);
ring.Qid = qid;
ring.Active = false;
int cmdVer = IwxLookupCmdVer(IWX_DATA_PATH_GROUP, IWX_SCD_QUEUE_CONFIG_CMD);
@@ -1016,14 +1076,280 @@ namespace Drivers::Net::Wifi {
return false;
auto* pkt = (IwxRxPacket*)g_iwx.CmdRespBuf;
auto* resp = (IwxTxQueueCfgRsp*)pkt->data;
// On the v3 data-path API the firmware owns queue assignment: `qid` is
// only a hint and the response names the queue we actually got.
if (resp->queue_number != qid) {
KernelLogStream(WARNING, "WiFi") << "Firmware assigned queue "
<< (uint64_t)resp->queue_number << ", expected " << (uint64_t)qid;
KernelLogStream(INFO, "WiFi") << "Firmware assigned TX queue "
<< (uint64_t)resp->queue_number << " (asked for "
<< (uint64_t)qid << ")";
}
ring.Qid = resp->queue_number;
ring.Active = true;
return true;
}
void IwxDisableTxq(IwxTxRing& ring, int staId, int tid) {
if (!ring.Active) return;
ring.Active = false;
int cmdVer = IwxLookupCmdVer(IWX_DATA_PATH_GROUP, IWX_SCD_QUEUE_CONFIG_CMD);
if (cmdVer == 3) {
IwxScdQueueCfgCmd cmd = {};
cmd.operation = IWX_SCD_QUEUE_REMOVE;
cmd.u.remove.sta_mask = 1u << staId;
cmd.u.remove.tid = (uint32_t)tid;
IwxSendCmdPdu(IWX_WIDE_ID(IWX_DATA_PATH_GROUP, IWX_SCD_QUEUE_CONFIG_CMD),
&cmd, sizeof(cmd));
} else {
IwxTxQueueCfgCmd cmd = {};
cmd.sta_id = (uint8_t)staId;
cmd.tid = (uint8_t)tid;
cmd.flags = 0; // clear ENABLE_QUEUE
cmd.cb_size = IWX_TFD_QUEUE_CB_SIZE(IWX_TX_RING_COUNT);
cmd.byte_cnt_addr = ring.BcTbl.Phys;
cmd.tfdq_addr = ring.Desc.Phys;
IwxSendCmdPdu(IWX_SCD_QUEUE_CFG, &cmd, sizeof(cmd));
}
IwxResetTxRing(ring);
}
// =========================================================================
// Frame transmission
// =========================================================================
// Lowest usable transmit antenna, as a rate_n_flags antenna field.
static uint32_t IwxTxAntBits() {
uint8_t ant = (uint8_t)((g_iwx.Fw.PhyConfig & IWX_FW_PHY_CFG_TX_CHAIN)
>> IWX_FW_PHY_CFG_TX_CHAIN_POS);
if (g_iwx.Nvm.ValidTxAnt) ant &= g_iwx.Nvm.ValidTxAnt;
if (!ant) ant = 1;
uint8_t lowest = (uint8_t)(ant & (uint8_t)(~ant + 1)); // isolate low bit
return (uint32_t)lowest << IWX_RATE_MCS_ANT_POS;
}
// The lowest basic rate for the current band, in whichever rate_n_flags
// encoding the firmware advertises: 1 Mbps CCK on 2.4 GHz, 6 Mbps OFDM on
// 5 GHz. Management frames go out at this rate because rate control has
// no table for the station until it is associated.
static uint32_t IwxLowestRate() {
uint32_t ant = IwxTxAntBits();
// Firmware exposing TX_CMD notification version 7 or later (equally,
// command version 9+) uses the "version 2" rate layout.
int notifVer = IwxLookupNotifVer(IWX_LONG_GROUP, IWX_TX_CMD);
int cmdVer = IwxLookupCmdVer(IWX_LONG_GROUP, IWX_TX_CMD);
bool v2 = notifVer > 6 || cmdVer >= 9;
if (v2) {
uint32_t mod = g_iwx.Is5GHz ? IWX_RATE_MCS_MOD_LEGACY_OFDM
: IWX_RATE_MCS_MOD_CCK;
return ant | mod | IWX_RATE_MCS_CHAN_WIDTH_20 | 0u; // index 0
}
uint32_t plcp = g_iwx.Is5GHz ? IWX_RATE_6M_PLCP : IWX_RATE_1M_PLCP;
uint32_t cck = g_iwx.Is5GHz ? 0 : IWX_RATE_MCS_CCK_MSK_V1;
return ant | cck | plcp;
}
// Recover a queue whose completions stopped arriving. Without this a
// single lost TX response would permanently consume a slot and, after
// StageSlots of them, wedge the queue.
static uint64_t g_txStallMs = 0;
static bool IwxTxQueueHasRoom(IwxTxRing& ring) {
if (ring.Queued < ring.StageSlots) {
g_txStallMs = 0;
return true;
}
uint64_t now = Timekeeping::GetMilliseconds();
if (g_txStallMs == 0) {
g_txStallMs = now;
return false;
}
if (now - g_txStallMs < 2000) return false;
KernelLogStream(WARNING, "WiFi")
<< "TX queue " << (uint64_t)ring.Qid
<< " stopped completing; resetting its outstanding count";
ring.Queued = 0;
g_txStallMs = 0;
return true;
}
bool IwxTxFrame(IwxTxRing& ring, const uint8_t* hdr, uint32_t hdrLen,
const uint8_t* payload, uint32_t payloadLen,
bool encrypt, bool fixedRate) {
if (g_iwx.State != IwxFwState::Running) return false;
if (!ring.Active || ring.StageSlots == 0) return false;
if (!hdr || hdrLen < IEEE80211_HDR_LEN || hdrLen > 32) return false;
// The 802.11 header is padded to a dword boundary before the payload;
// TX_CMD_OFFLD_PAD tells the firmware to skip those bytes.
uint32_t padded = (hdrLen + 3) & ~3u;
uint32_t head = (uint32_t)(sizeof(IwxCmdHeader) + sizeof(IwxTxCmdGen3)) + padded;
if (head + payloadLen > 4096) return false;
g_iwx.TxLock.Acquire();
if (!IwxTxQueueHasRoom(ring)) {
g_iwx.TxLock.Release();
return false;
}
uint32_t idx = ring.Cur;
uint32_t slot = idx % ring.StageSlots;
uint8_t* buf = ring.Stage[slot];
uint64_t phys = ring.StagePhys[slot];
memset(buf, 0, head);
auto* ch = (IwxCmdHeader*)buf;
ch->code = IWX_TX_CMD;
ch->flags = 0; // TX_CMD stays in the legacy group
ch->idx = (uint8_t)idx;
ch->qid = (uint8_t)ring.Qid;
auto* tx = (IwxTxCmdGen3*)(buf + sizeof(IwxCmdHeader));
tx->len = (uint16_t)(hdrLen + payloadLen);
uint16_t flags = 0;
if (!encrypt) flags |= IWX_TX_FLAGS_ENCRYPT_DIS;
if (fixedRate) {
flags |= IWX_TX_FLAGS_CMD_RATE;
tx->rate_n_flags = IwxLowestRate();
}
tx->flags = flags;
uint32_t offload = ((hdrLen / 2) & IWX_TX_CMD_OFFLD_MH_MASK)
<< IWX_TX_CMD_OFFLD_MH_SIZE_POS;
if (hdrLen % 4) offload |= IWX_TX_CMD_OFFLD_PAD;
tx->offload_assist = offload;
uint8_t* body = buf + sizeof(IwxCmdHeader) + sizeof(IwxTxCmdGen3);
memcpy(body, hdr, hdrLen);
if (payloadLen) memcpy(body + padded, payload, payloadLen);
auto* desc = &((IwxTfhTfd*)ring.Desc.Virt)[idx];
memset(desc, 0, sizeof(*desc));
desc->tbs[0].tb_len = (uint16_t)IWX_FIRST_TB_SIZE;
desc->tbs[0].addr = phys;
desc->tbs[1].tb_len = (uint16_t)(head - IWX_FIRST_TB_SIZE);
desc->tbs[1].addr = phys + IWX_FIRST_TB_SIZE;
uint16_t numTbs = 2;
if (payloadLen) {
desc->tbs[2].tb_len = (uint16_t)payloadLen;
desc->tbs[2].addr = phys + head;
numTbs = 3;
}
desc->num_tbs = numTbs;
// Byte-count table: AX210 wants the frame length in bytes plus the
// number of extra 64-byte chunks the firmware must fetch for the TFD.
uint32_t filled = (uint32_t)(sizeof(uint16_t) + numTbs * sizeof(IwxTfhTb));
uint32_t chunks = ((filled + 63) / 64) - 1;
auto* bc = (IwxGen3BcTblEntry*)ring.BcTbl.Virt;
bc[idx].tfd_offset = (uint16_t)((hdrLen + payloadLen) | (chunks << 14));
asm volatile("" ::: "memory");
ring.Queued++;
ring.Cur = (ring.Cur + 1) % IWX_TX_RING_COUNT;
ring.CurHw = (ring.CurHw + 1) % IWX_TFD_QUEUE_SIZE_MAX_GEN3;
IwxWrite32(IWX_HBUS_TARG_WRPTR, ((uint32_t)ring.Qid << 16) | ring.CurHw);
g_iwx.TxLock.Release();
return true;
}
void IwxTxComplete(int qid, int idx, uint32_t status) {
(void)idx;
IwxTxRing* ring = nullptr;
if (g_iwx.MgmtQ.Active && qid == g_iwx.MgmtQ.Qid) ring = &g_iwx.MgmtQ;
if (!ring) return;
g_iwx.TxLock.Acquire();
if (ring->Queued > 0) ring->Queued--;
g_iwx.TxLock.Release();
if (status == IWX_TX_STATUS_SUCCESS || status == IWX_TX_STATUS_DIRECT_DONE)
g_iwx.TxPackets++;
else
g_iwx.TxFailures++;
}
// =========================================================================
// Hardware key installation
// =========================================================================
// Keys go in and out through SEC_KEY_CMD: the MLD firmware does not
// implement the legacy ADD_STA_KEY, like the rest of the legacy station
// API. Values mirror the Linux trace: PTK as {sta_mask 1, key_id 0,
// flags CIPHER}, GTK as {sta_mask 1, key_id N, flags CIPHER|MCAST}.
static uint32_t SecKeyFlags(uint8_t cipher, uint32_t keyLen, bool pairwise) {
uint32_t flags;
switch (cipher) {
case RSN_CIPHER_CCMP:
case RSN_CIPHER_CCMP_256:
flags = IWX_SEC_KEY_FLAG_CIPHER_CCMP;
break;
case RSN_CIPHER_GCMP:
case RSN_CIPHER_GCMP_256:
flags = IWX_SEC_KEY_FLAG_CIPHER_GCMP;
break;
default:
return 0;
}
if (keyLen == 32) flags |= IWX_SEC_KEY_FLAG_KEY_SIZE;
if (!pairwise) flags |= IWX_SEC_KEY_FLAG_MCAST_KEY;
return flags;
}
bool IwxSetKey(const uint8_t* key, uint32_t keyLen, uint8_t keyIdx,
bool pairwise, uint8_t cipher, const uint8_t* rsc) {
if (!key || (keyLen != 16 && keyLen != 32)) return false;
uint32_t flags = SecKeyFlags(cipher, keyLen, pairwise);
if (!flags) {
KernelLogStream(WARNING, "WiFi")
<< "Cannot install a key for cipher " << (uint64_t)cipher;
return false;
}
IwxSecKeyCmd cmd = {};
cmd.action = IWX_FW_CTXT_ACTION_ADD;
cmd.sta_mask = 1u << IWX_STATION_ID;
cmd.key_id = keyIdx;
cmd.key_flags = flags;
memcpy(cmd.key, key, keyLen);
// The EAPOL RSC carries the AP's packet number for the group key,
// lowest byte first; it becomes the initial receive counter.
if (rsc) {
uint64_t pn = 0;
for (int i = 5; i >= 0; i--) pn = (pn << 8) | rsc[i];
cmd.rx_seq = pn;
}
return IwxSendCmdPdu(IWX_WIDE_ID(IWX_DATA_PATH_GROUP, IWX_SEC_KEY_CMD),
&cmd, sizeof(cmd));
}
bool IwxRemoveKey(uint8_t keyIdx, bool pairwise, uint8_t cipher,
uint32_t keyLen) {
uint32_t flags = SecKeyFlags(cipher, keyLen, pairwise);
if (!flags) return false;
IwxSecKeyCmd cmd = {};
cmd.action = IWX_FW_CTXT_ACTION_REMOVE;
cmd.sta_mask = 1u << IWX_STATION_ID;
cmd.key_id = keyIdx;
cmd.key_flags = flags;
return IwxSendCmdPdu(IWX_WIDE_ID(IWX_DATA_PATH_GROUP, IWX_SEC_KEY_CMD),
&cmd, sizeof(cmd));
}
// =========================================================================
// RX / notification processing
// =========================================================================
@@ -1083,7 +1409,24 @@ namespace Drivers::Net::Wifi {
// between them they pin down which host command the firmware rejected.
void IwxDumpFwError() {
KernelLogStream(ERROR, "WiFi-FW") << "Firmware assert; last command sent: 0x"
<< base::hex << (uint64_t)g_iwx.LastCmdId << base::dec;
<< base::hex << (uint64_t)g_iwx.LastCmdId << base::dec
<< " (" << (uint64_t)g_iwx.LastCmdLen << " byte payload)";
// Dump the payload: a struct that does not match the firmware's
// expected layout is otherwise impossible to spot from the log.
{
uint32_t n = g_iwx.LastCmdLen;
if (n > sizeof(g_iwx.LastCmdPayload)) n = sizeof(g_iwx.LastCmdPayload);
for (uint32_t off = 0; off < n; off += 32) {
auto line = KernelLogStream(INFO, "WiFi-FW");
line << " +" << (uint64_t)off << ": " << base::hex;
for (uint32_t i = off; i < n && i < off + 32; i++) {
if (g_iwx.LastCmdPayload[i] < 0x10) line << "0";
line << (uint64_t)g_iwx.LastCmdPayload[i];
}
line << base::dec;
}
}
uint32_t base_ = g_iwx.UmacErrorTable;
if (base_ < 0x400000) {
@@ -1141,6 +1484,16 @@ namespace Drivers::Net::Wifi {
case IWX_WIDE_ID(IWX_REGULATORY_AND_NVM_GROUP, IWX_PNVM_INIT_COMPLETE):
g_iwx.InitComplete |= 0x2;
break;
case IWX_TX_CMD: {
// TX completion for a frame we queued on a data/mgmt queue.
uint32_t status = 0;
if (IwxRxPacketPayloadLen(pkt) >= sizeof(IwxTxResp)) {
auto* r = (const IwxTxResp*)pkt->data;
status = r->status & IWX_TX_STATUS_MSK;
}
IwxTxComplete(qid & ~0x80, pkt->hdr.idx, status);
break;
}
case IWX_REPLY_ERROR: {
if (IwxRxPacketPayloadLen(pkt) >= 8) {
uint32_t errType = *(const uint32_t*)pkt->data;
@@ -1208,8 +1561,9 @@ namespace Drivers::Net::Wifi {
void IwxProcessEvents() {
if (!g_iwx.Mmio) return;
if (g_iwx.InProcessEvents) return; // never nest
g_iwx.InProcessEvents = true;
// Never nest. With interrupts enabled during command waits this is a
// genuine multi-core race, so it has to be an atomic test-and-set.
if (g_iwx.InProcessEvents.test_and_set(std::memory_order_acquire)) return;
if (g_msix) {
uint32_t fh = IwxRead32(IWX_CSR_MSIX_FH_INT_CAUSES_AD);
@@ -1276,7 +1630,7 @@ namespace Drivers::Net::Wifi {
IwxNotifIntr();
g_iwx.WorkPending = false;
g_iwx.InProcessEvents = false;
g_iwx.InProcessEvents.clear(std::memory_order_release);
}
// =========================================================================
@@ -1435,7 +1789,8 @@ namespace Drivers::Net::Wifi {
|| !IwxDmaAlloc(g_iwx.PrphInfo, 4096)
|| !IwxAllocRxRing()
|| !IwxAllocTxRing(g_iwx.CmdQ, IWX_DQA_CMD_QUEUE)
|| !IwxAllocTxRing(g_iwx.MgmtQ, IWX_DQA_MGMT_QUEUE)) {
|| !IwxAllocTxRing(g_iwx.MgmtQ, IWX_DQA_MGMT_QUEUE,
IWX_TX_STAGE_SLOTS)) {
KernelLogStream(ERROR, "WiFi") << "Could not allocate device DMA memory";
IwxFreeRxRing();
IwxFreeTxRing(g_iwx.CmdQ);