Skip to content
HN On Hacker News ↗

Wanted to download an ISO, ended up writing a kernel exploit

▲ 370 points • 191 comments • by 0xdeafbeef • 3w ago • HN discussion ↗

Pangram verdict · v3.3

We believe that this text is a mix of AI and human-written content.

55 %

AI likelihood · overall

Mixed
44% human-written 56% AI-generated
SEGMENTS · HUMAN 2 of 5
SEGMENTS · AI 1 of 5
WORD COUNT 1,319
PEAK AI % 76% · §2
Analyzed
Oct 15
backend: pangram/v3.3
Segments scanned
5 windows
avg 264 words each
Distribution
44 / 56%
human / AI fraction
Verdict
Mixed
Pangram v3.3

Article text · 1,319 words · 5 segments analyzed

Human AI-generated
§1 Human · 12%

TL;DR: My TP-Link EAP770 v1 corrupts downloads over Wi-Fi and TP-Link EOL'd it before I unboxed it. The fix is two zeros in sysfs. TP-Link's shell won't let me write them, so I wrote a kernel exploit for my own fucking access point. Long downloads kept breaking# In August 2023 I bought a shiny new Wi-Fi 7 access point, a TP-Link EAP770. The connection was a bit janky, a video would occasionally start buffering, but it didn't bother me much. I mostly worked on a workstation connected over Ethernet. When I needed to go somewhere, I'd rsync my ~ to the laptop over Wi-Fi and take it with me. Those transfers started failing on files larger than a few GiB. SSH was reporting MAC errors, so I spent a while debugging rsync and SSH. Everything else worked, changing rsync options made it better or worse, Wireshark showed zero bad checksums and zero retransmissions, and a single UDP stream of random bytes came through clean. I trusted TCP to deliver the right bytes because that's what the fucking thing is for, so I was sure the problem was in rsync or SSH. I wrote a wrapper around rsync with retries and moved on. Later I happened to start using Tailscale for traffic inside my home network, and the transfers stopped needing retries. WireGuard drops a packet if its authentication fails, so the TCP connection inside the tunnel sees a lost packet and retransmits it. Since the transfers worked, I stopped digging. Zero firmware updates# The device is an EAP770(EU) v1.0 with a Qualcomm IPQ9574 running Linux 5.4.164 and firmware 1.0.0 Build 20230605 Rel. 71641. That's the firmware it shipped with, and apparently the last time TP-Link remembered this thing even existed :) I bought this thing in August 2023, and the Wi-Fi 7 standard was only finalized in September 2024. It has no Wi-Fi Alliance certification, and buying it was stupid of me. PLZ DON'T BUY STUFF UNTIL STANDARDS ARE FINALIZED AND DEVICES ARE CERTIFIED. In 2025 I noticed that everything else was getting updates while this thing was still on its original firmware. I contacted support in June because the website only had firmware for v2. They couldn't find v1 in their database and asked for photos of the device and box before sending me this: The EAP770 Version 1 is an End-of-Life product. While it may still be available through some retailers, it is no longer supported with firmware updates. NICE. Tracking down the corruption# In 2026 I decided to switch to NixOS and found that a 128 MiB file from the Nix binary cache on Fastly would fail over HTTP/2 but downloaded fine over HTTP/1.1. I still don't know why HTTP/1.1 helped; my guess is that HTTP/2 was simply faster. git clones also failed, and HTTPS connections died with bad record mac. In March, downloading a NixOS ISO with wget died with a TLS error: 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. I forced HTTP/1.1 and left it alone again. In September I turned the workstation into a build farm and switched to the laptop, which connects over Wi-Fi. Wi-Fi was also dropping every day around 11:00, right before my daily call. After it had died right before the call several days in a row, I decided to open this Pandora's box again and sat down with Opus 5.5 to trace what was happening. Spoiler alert: the drops turned out to be a separate bug. I covered the laptop's Qualcomm driver in BPF probes and ran apstats on the AP as root. The bug is in the closed-source firmware on the laptop's Wi-Fi card; see the update at the bottom. I had tried enabling and disabling Wi-Fi 7, turning off GRO on the client, and replacing the MediaTek adapter in my Framework 16 with a Qualcomm one.

§2 AI · 76%

The new adapter and disabling GRO seemed to help, but none of this stopped the corruption. I captured a failing HTTPS download with tcpdump and found a 1440-byte TCP segment with 64 wrong bytes in a row and a valid TCP checksum. TcpInCsumErrors stayed at zero. TCP accepted the segment and handed it to TLS, which rejected the record with bad record mac. The damaged record wouldn't decrypt, so I couldn't inspect the bad bytes against the expected payload. I switched to raw TCP between a wired machine and the Wi-Fi laptop, sending known data that I could compare byte for byte. The corruption only appeared with several TCP streams running at once, in the LAN-to-Wi-Fi direction. The test found three bad 64-byte blocks in 8192 MiB, each starting with zeros. A valid checksum on a damaged segment meant something had recalculated it after the corruption, so the question became what on the path rewrites TCP headers. The corruption still showed up after swapping the MediaTek card for the Qualcomm one, so it wasn't the laptop. Wired clients were clean and the CCMP MIC on the bad frames was intact, so it wasn't the cable and it wasn't the air. That left the bridge between Ethernet and Wi-Fi inside the AP. 64 bytes is a cache line on the IPQ9574's cores, which points at a cache or DMA bug rather than a flipped bit. The only thing on that path that both accelerates bridged traffic and recalculates checksums is Qualcomm's SFE/PPE offload. Two writes that need root# Reading the driver source suggested disabling SFE/PPE with these two settings: echo 0 > /sys/sfe/l2_feature echo 0 > /sys/sfe/ppe_rfs_feature Both sysfs files are writable only by root, but TP-Link's SSH shell runs as uid 1 with no capabilities. There's no GUI setting for this either. There's also no v1 firmware image to patch and flash, and the v2 firmware is for different hardware. I tried the sibling EAP773's EAP773(EU)_V1_1.0.14, and the device rejected it. Newer v2 firmware releases are encrypted. Flashing the chip directly needed a flash clip. I didn't want to hunt one down or brick the fucking AP.

§3 Human · 18%

I wasn't going to spend another $200 replacing an AP because TP-Link couldn't be bothered to maintain the one they had already sold me. It's sitting in my house, I know which setting to change, and their software won't let me. Shout-out to TP-Link though: the system is pretty hardened. I found zero vectors in their own binaries, they use whitelists everywhere and load only the modules they need. So no free ; echo 0 > /sys/sfe/ppe_rfs_feature cheese for me. So the way in had to be the kernel. With Opus 4.8, I put together a chain using CVE-2022-0185 and pipe-buffer manipulation.

§4 Mixed · 68%

CVE-2022-0185 is an integer underflow in legacy_parse_param(), reachable from an unprivileged user namespace, that lets a write overflow a 4 KiB slab object. The pipe-buffer manipulation turns that overflow into an arbitrary write. There were already public write-ups and PoCs for these. The exploit had to leave the AP running because the broken offload comes back after a reboot. The overflow leaves the 4 KiB slab in a state where a second attempt panics, so there's one shot per boot.

§5 Mixed · 31%

I gave Opus guidance on the primitives, then had it iterate until the exploit was stable. Exploit details Without a v1 GPL source drop, I used the nearest published source, for the EAP783, whose kernel config differs from the AP's /proc/config.gz by three cosmetic lines. I built a QEMU harness with that config, got the chain working there, and then moved to the real device. The kernel has no KASLR, so addresses stay the same between boots, but kallsyms is zeroed and I still had to find the addresses at runtime. The QEMU build loaded the kernel at physical 0x40080000, but the device loaded it at 0x42080000, putting the calculations off by 0x2000000. The correct address was in a build config variable in the EAP783 source.