Wilde een ISO downloaden, eindigde met het schrijven van een kernel-exploit
Lange downloads bleven mislukken
In augustus 2023 kocht ik een gloednieuw Wi-Fi 7 access point: de TP-Link EAP770. De verbinding was wat instabiel; een video begon af en toe te bufferen, maar dat stoorde me niet echt. Ik werkte voornamelijk op een workstation die via Ethernet was aangesloten. Wanneer ik ergens naartoe moest, gebruikte ik rsync om mijn home-directory via Wi-Fi naar mijn laptop over te zetten.
Deze transfers begonnen echter te mislukken bij bestanden groter dan een paar GiB. SSH rapporteerde MAC-fouten, dus ik besteedde een tijd aan het debuggen van rsync en SSH. Alles overig werkte; het wijzigen van rsync-opties maakte het beter of slechter, Wireshark toonde nul foute checksums en nul hertransmissies, en een enkele UDP-stream van willekeurige bytes kwam schoon aan.
Ik vertrouwde erop dat TCP de juiste bytes zou leveren, want dat is waar TCP voor bedoeld is, dus ik was ervan overtuigd dat het probleem in rsync of SSH zat. Ik schreef een wrapper rond rsync met retries en liet het daarbij. Later begon ik Tailscale te gebruiken voor verkeer binnen mijn thuisnetwerk, waarna de transfers geen retries meer nodig hadden. WireGuard gooit namelijk een pakket weg als de authenticatie mislukt, waardoor de TCP-verbinding in de tunnel een verloren pakket ziet en dit opnieuw verzendt. Omdat de transfers nu werkten, stopte ik met zoeken.
Geen firmware-updates
Het apparaat is een EAP770(EU) v1.0 met een Qualcomm IPQ9574, draaiend op Linux 5.4.164 en firmware 1.0.0 Build 20230605 Rel. 71641. Dat is de firmware waarmee hij werd geleverd, en blijkbaar de laatste keer dat TP-Link zich herinnerde dat dit apparaat bestond.
Ik kocht dit apparaat in augustus 2023, terwijl de Wi-Fi 7-standaard pas in september 2024 werd gefinaliseerd. Het heeft geen Wi-Fi Alliance-certificering; het was dom van me om het te kopen. Tip: koop geen apparatuur totdat standaarden zijn gefinaliseerd en apparaten zijn gecertificeerd.
In 2025 merkte ik dat alle andere apparaten updates kregen, behalve dit ding. In juni nam ik contact op met de support, omdat de website alleen firmware voor v2 had. Ze konden v1 niet in hun database vinden en vroegen om foto's van het apparaat en de doos, om me vervolgens dit te sturen:
"De EAP770 Versie 1 is een End-of-Life product. Hoewel het mogelijk nog verkrijgbaar is via sommige retailers, wordt het niet langer ondersteund met firmware-updates."
Geweldig.
De corruptie opsporen
In 2026 besloot ik over te stappen op NixOS en ontdekte dat een bestand van 128 MiB vanuit de Nix binary cache op Fastly zou mislukken via HTTP/2, maar prima downloadde via HTTP/1.1. Ik weet nog steeds niet waarom HTTP/1.1 hielp; mijn vermoeden is dat HTTP/2 simpelweg sneller was. git clones mislukten ook, en HTTPS-verbindingen vielen weg met een "bad record mac".
In maart stierf het downloaden van een NixOS ISO met wget door een TLS-fout: 2026-03-05 12:37:18 (31.6 MB/s) - Read error at byte 2722477636/3683778560 (error:0A000119:SSL routines::decryption failed or bad record mac; error:0A000139:SSL routines::record layer failure). Retrying.
Ik forceerde HTTP/1.1 en liet het weer rusten. In september maakte ik van het workstation een build farm en stapte over op de laptop via Wi-Fi. De Wi-Fi viel nu ook elke dag rond 11:00 uur uit, vlak voor mijn dagelijkse call. Nadat dit meerdere dagen achter elkaar gebeurde, besloot ik de doos van Pandora opnieuw te openen en met Opus 5.5 te onderzoeken wat er aan de hand was. (Spoiler: de uitval bleek een aparte bug te zijn).
Ik plaatste BPF-probes op de Qualcomm-driver van de laptop en draaide apstats als root op het AP. De bug zit in de closed-source firmware op de Wi-Fi-kaart van de laptop (zie update onderaan).
Ik had geprobeerd Wi-Fi 7 aan- en uit te zetten, GRO op de client uit te schakelen en de MediaTek-adapter in mijn Framework 16 te vervangen door een Qualcomm-adapter. De nieuwe adapter en het uitschakelen van GRO leken te helpen, maar niets stopte de corruptie.
Met tcpdump legde ik een mislukte HTTPS-download vast en vond een TCP-segment van 1440 bytes met 64 opeenvolgende foute bytes, maar met een geldige TCP-checksum. TcpInCsumErrors bleef op nul. TCP accepteerde het segment en gaf het door aan TLS, dat het record weigerde vanwege een "bad record mac". Omdat het beschadigde record niet kon worden ontsleuteld, kon ik de foute bytes niet vergelijken met de verwachte payload.
Ik stapte over op ruwe TCP tussen een bekabelde machine en de Wi-Fi-laptop, waarbij ik bekende data verstuurde. De corruptie trad alleen op wanneer er meerdere TCP-streams tegelijk liepen, in de richting LAN-naar-Wi-Fi. De test vond drie foute blokken van 64 bytes in 8192 MiB, elk beginnend met nullen.
Een geldige checksum op een beschadigd segment betekende dat iets de checksum opnieuw had berekend na de corruptie. De vraag werd dus: wat op het pad schrijft TCP-headers opnieuw? De corruptie bleef bestaan na het wisselen van de netwerkkaart, dus het lag niet aan de laptop. Bekabelde clients waren schoon en de CCMP MIC op de foute frames was intact, dus het lag niet aan de kabel of de lucht.
Dat liet de bridge tussen Ethernet en Wi-Fi in het AP over. 64 bytes is een cache-line op de cores van de IPQ9574, wat wijst op een cache- of DMA-bug in plaats van een bit-flip. Het enige onderdeel op dat pad dat zowel bridged verkeer versnelt als checksums opnieuw berekent, is de SFE/PPE-offload van Qualcomm.
Twee schrijfacties waarvoor root nodig is
Uit de broncode van de driver bleek dat SFE/PPE uitgeschakeld kan worden met deze twee instellingen: echo 0 > /sys/sfe/l2feature echo 0 > /sys/sfe/pperfs_feature
Beide sysfs-bestanden zijn alleen beschrijfbaar door root, maar de SSH-shell van TP-Link draait als uid 1 zonder capabilities. Er is geen GUI-instelling hiervoor. Er is ook geen v1-firmware-image om te patchen en te flashen, en de v2-firmware is voor andere hardware. Ik probeerde de firmware van de zustermodel EAP773, maar het apparaat weigerde deze. Nieuwere v2-firmware-releases zijn versleuteld. Het direct flashen van de chip vereiste een flash-clip; ik wilde het risico niet lopen om het AP te bricken.
Ik was niet bereid om opnieuw 200 dollar uit te geven aan een nieuw AP omdat TP-Link het niet kon opbrengen om het product dat ze al verkocht hadden te onderhouden. Het apparaat staat in mijn huis, ik weet welke instelling ik moet wijzigen, en hun software staat me dat niet toe.
Complimenten aan TP-Link: het systeem is behoorlijk goedbeveiligd. Ik vond geen vectors in hun eigen binaries; ze gebruiken overal whitelists en laden alleen de modules die ze nodig hebben. Dus geen gratis ; echo 0 > /sys/sfe/pperfsfeature voor mij. De enige weg naar binnen was de kernel.
Met Opus 4.8 stelde ik een chain samen met CVE-2022-0185 en pipe-buffer manipulatie. CVE-2022-0185 is een integer underflow in legacyparseparam(), bereikbaar vanuit een onbevoorrecht user namespace, waardoor een write een 4 KiB slab-object kan overschrijven. De pipe-buffer manipulatie zet die overflow om in een arbitrary write. Er waren al publieke write-ups en PoC's voor deze technieken beschikbaar.
De exploit moest het AP draaiende houden, omdat de defecte offload terugkeert na een reboot. De overflow laat de 4 KiB slab in een staat achter waardoor een tweede poging tot een panic leidt; er is dus één kans per boot. Ik gaf Opus instructies over de primitives en liet het itereren tot de exploit stabiel was.
Details van de exploit
Zonder een v1 GPL-broncode gebruikte ik de dichtstbijzijnde gepubliceerde bron, die van de EAP783. De kernelconfig verschilt van de /proc/config.gz van het AP met slechts drie cosmetische regels. Ik bouwde een QEMU-harness met die config, kreeg de chain daar werkend en verplaatste deze vervolgens naar het echte apparaat.
De kernel heeft geen KASLR, dus adressen blijven gelijk tussen boots, maar kallsyms is genuld, waardoor ik de adressen tijdens runtime moest vinden. De QEMU-build laadde de kernel op fysiek 0x40080000, maar het apparaat laadde hem op 0x42080000, waardoor de berekeningen met 0x2000000 verschilden. Het juiste adres stond in een build-config variabele in de EAP783-bron. Zelfs na correctie van de basis waren .text, .rodata en .data elk met een ander bedrag verschoven ten opzichte van de QEMU-build.
De exploit werkt als volgt:
unsharenaar een user namespace zodatfsopenbereikbaar is en de underflow getriggerd kan worden.- Defragmenteer de 4 KiB slab met enkele honderden allocaties van dezelfde grootte, en groom deze om de overflow-bron, een victim key, een SysV-bericht en pipe-rings naast elkaar te plaatsen.
- Overflow in de metadata van de victim key en vergroot het lengteveld.
- Door de key terug te lezen, lekken naburige objecten, waaronder
buf[0].opsvan een pipe, wat het adres vananonpipebuf_opsbevat. - Richt de buffer van één pipe om naar de ring-page van een andere pipe, en vervals een entry in die tweede ring met de gelekte ops-pointer.
pipebufcan_merge()controleert of de ops-pointer overeenkomt, dus een merge-write via de vervalste entry kan een willekeurige fysieke page bereiken, inclusief een page van de kernel-image.- Scan
.dataommodprobe_pathte vinden. - Het lezen van kernel-image pages via een pipe roept normaal
putpage()op, wat leidt tot een Bad page map en een panic. Maarpipereadroept alleen->confirmen->releaseaan en controleert niet of de ops-tabel echt is. Een vervalstepipebufoperationswaarbij beide entries naargenericpipebuf_confirmwijzen (wat nul teruggeeft zonder iets te doen), maakt het mogelijk de pages te lezen zonder ze vrij te geven. - Loop door
.datain batches tot de string is gevonden. - Overschrijf
modprobe_pathen voer een bestand uit viaexecvemet magic-bytes die geen enkel binair formaat matchen. - De kernel roept
request_module()aan, wat het vervangende pad uitvoert als globale uid 0. - Houd een child-proces in leven dat de vervalste pipe vasthoudt; als het proces afsluit, wordt de pipe afgebroken en volgt er een panic op de gereserveerde page.
Het werkend krijgen hiervan kost ongeveer zeventien uur aan "token gambling" met Opus 4.8, inclusief het lezen van de binaries in radare2, het controleren van de actieve protecties en het oplossen van crashes. Het AP bleef online en het uitschakelen van de offload stopte de corruptie.
Een DMA-cache-coherentiebug
De qca-nss-dp driver van Qualcomm hergebruikt pakketbuffers zonder de CPU-cache voor de volledige receive-buffer te synchroniseren voordat deze teruggegeven wordt aan de NIC. Op deze hardware schrijft DMA direct naar DRAM zonder de CPU-caches synchroon te houden.
Een "dirty" cache-line die is overgebleven van het vorige gebruik van een buffer kan worden teruggeschreven nadat de NIC een nieuw pakket heeft ontvangen, waardoor een deel van dat pakket in het geheugen wordt overschreven. SFE/PPE berekent vervolgens de checksums over de gecorrumpeerde payload, waardoor de client een nieuwe TCP-checksum krijgt die geldig is voor de onzin-data. TCP accepteert dit; de end-to-end MACs in TLS en SSH vangen de schade echter op en rapporteren een "bad record mac" of gecorrumpeerde MAC.
Op x86 houdt de hardware DMA coherent met de CPU-caches, dus hetzelfde gebrek aan cache-onderhoud zou daar niets corrumperen. Tijdens het doorzoeken van de kernel-sources vond ik soortgelijke DMA-cache-problemen in verschillende andere drivers.
Driver-details
Dit is de out-of-tree NSS-code van Qualcomm, beschikbaar in de openwrt/qca-nss-dp mirror. SFE's fast transmit path maakt skb_headlen(skb) bytes schoon vanuit de huidige skb->data, wat het lineaire deel van het uitgaande pakket beslaat.
In edma_tx.c (regels 483-490) gebeurt dit:
buff_addr = (dma_addr_t)virt_to_phys(skb->data);
EDMA_TXDESC_BUFFER_ADDR_SET(txd, buff_addr);
// ... (ondersteuning voor 40-bit)
dmac_clean_range_no_dsb((void *)skb->data, (void *)(skb->data + buf_len));
Vervolgens wordt skb->fastrecycled = 1 gezet en wordt de buffer teruggegeven aan een per-CPU recycle-lijst zonder de inhoud te wissen (edmatx.c regels 735-743).
Wanneer die buffer opnieuw wordt gebruikt voor ontvangst, ziet het refill-pad de vlag en slaat de invalidatie over: In edma_rx.c (regels 477-491):
if (unlikely(!skb->fast_recycled)) {
dmac_inv_range_no_dsb((void *)skb->data,
(void *)(skb->data + rx_alloc_size -
EDMA_RX_SKB_HEADROOM -
NET_IP_ALIGN));
}
skb->fast_recycled = 0;
De receive-regio is 1856 bytes (29 cache-lines). De vorige transmit heeft echter slechts twee of drie lines schoongemaakt vanaf een andere offset. Dirty lines buiten dat bereik kunnen worden teruggeschreven na de DMA van de NIC, waardoor telkens 64 bytes van het ontvangen pakket worden vervangen.
Zodra de dirty data het pakket in DRAM heeft overschreven, kan het invalideren van de CPU-cache de ontvangen bytes niet meer herstellen. De NIC verifieert de checksum van het originele pakket vóór die overschrijving, en het receive-pad van het AP markeert het als CHECKSUM_UNNECESSARY. De exacte volgorde binnen de PPE is verborgen in proprietary hardware, maar de packet capture laat zien dat de checksum werd berekend na de corruptie.
Buffer-recycling is ingeschakeld via CONFIGSKBRECYCLER, een compile-time optie zonder runtime-switch. Het uitschakelen van de offload zorgt ervoor dat buffers het fast transmit path niet meer nemen, waardoor de driver fast_recycled niet meer zet en de volledige buffer wordt geïnvalideerd voordat deze aan de NIC wordt gegeven.
Opus 5.5 vond de bug en stopte vervolgens met praten
Toen ik overging van het diagnosticeren van de corruptie naar het toepassen van de fix, begon Opus 5.5 sessies af te breken, ongeveer elke tweede melding. Het noemen van iets als "local privilege escalation" is voldoende. Ik heb het Cyber Verification Program op mijn account geactiveerd, maar dat hielp niet. Opus 4.8 werkte door de exploitatie heen zonder dit probleem. De guards van 5.5 killen de sessie zelfs als ik hem alleen vraag om dit artikel te lezen. GPT-6 Astra leest en bewerkt hetzelfde artikel zonder enige cyber-verificatie.
Voor en na
Ik heb ruwe TCP-transfers getest tussen dezelfde twee machines met de offload ingeschakeld en uitgeschakeld.
| Offload | Richting | Data | Snelheid | Foute 64-byte blokken |
|---|---|---|---|---|
| Aan | LAN → Wi-Fi | 8192 MiB | 103.4 MiB/s | 3 |
| Uit | LAN → Wi-Fi | nachtelijke run | 95.4 MiB/s | 0 |
Het uitschakelen van de offload kostte in deze test ongeveer 8% van de downloadsnelheid. Ik moet prestaties opofferen om correcte data te krijgen, omdat TP-Link me niet toestaat mijn eigen apparaat fatsoenlijk te repareren.
Met de offload aan kon ik nooit een groot bestand over Wi-Fi in één stream downloaden; alleen chunked downloads lukten. Met de offload uit was de nachtelijke run volledig schoon en zijn grote downloads sindsdien altijd geslaagd. Uploads van Wi-Fi naar LAN waren al schoon vóór de fix.
De NOTSUPPORTBRIDGE teller in /sys/sfe/exceptions loopt op na het uitschakelen van de offload, omdat bridged flows nu het normale pad nemen in plaats van het versnelde pad. De instellingen worden na een reboot gereset, dus een watcher op mijn NAS pollt het AP en voert de exploit opnieuw uit wanneer de offload weer aan staat. Als de poging mislukt, reboot hij het AP via de management-API van TP-Link en probeert het opnieuw, met een harde limiet op het aantal reboots.
Qualcomm kan hun driver fixen als ze willen. OpenWrt heeft deze bug niet, en TP-Link zal sowieso geen nieuwe firmware voor mijn AP uitbrengen. De vergelijkbare DMA-bugs die ik in Linux vond, verdienen echter een nadere blik.
---
Update: de drops opsporen
De Qualcomm-kaart die ik in de Framework 16 heb geplaatst is een WCN785x. Elke paar uur stopte hij met het verzenden van data voor 1 tot 4 minuten. Aangezien ik root op het AP had, draaide ik apstats op de EAP770 samen met BPF-probes op de laptop en ving drie drops met logs van beide kanten.
Alles wat het AP naar de laptop stuurde kwam aan en werd bevestigd (ACK), dus de radio van de laptop zände en de receiver van het AP werkte. Alleen data-frames vanuit de laptop stopten met vertrekken. Op de laptop bleven pakketten naar ath12k gaan en de queue groeide naar duizenden frames. De firmware bevestigde een paar frames per keer en gaf de rest uiteindelijk terug aan de driver met CMDREMOVEAGED_FRAMES.
Op het AP bleef rx_data op +0 staan, met nul RX-fouten en nul retries; de frames verlieten de kaart simpelweg niet. Daarna blokkeerde de kaart zichzelf open en verstuurde hij de hele backlog in twee seconden.
Het signaal was -31 tot -42 dBm, er draaide geen scan en de IP-laag op de laptop was prima tijdens de drops. Ik gebruik de nieuwste upstream firmware (WLAN.HMT.1.1.c7-00108 UPSTREAM-3). Omdat ath12k alleen statusmeldingen terugkrijgt en alles over wanneer een frame in de lucht gaat wordt beslist in de closed-source amss.bin van Qualcomm, ligt het aan de firmware.
Ik weet nog steeds niet waarom hij stopt met zenden. De bursts die tijdens een drop door kwamen, lijken op 802.11ax/be uplink scheduling (trigger-based uplink of MU EDCA), maar ik vond geen instellingen op het AP om dit te controleren. Ik heb de 5 GHz radio van het AP omgezet van Wi-Fi 7 naar Wi-Fi 6 (802.11ax) en een watcher voor 24 uur laten draaien. Als de drops stoppen, is het een Wi-Fi 7-bug en blijf ik op Wi-Fi 6. Zo niet, dan is de firmware sowieso kapot en is het tijd voor een bugrapport of een Intel-kaart.
Groetjes,