Every access point in the house went unusable at once — pages timing out, video stuck at 240p, 46–60% packet loss even pinging the gateway one hop away — while every wired client stayed perfectly fine. The cause turned out to be a single Fire TV Stick, a router answering one of its queries with an empty packet, and a Fire OS bug that has been open and unfixed since July 2026: no backoff on that empty reply, so the device just keeps asking, forever, at close to 300 packets a second.
Confirm it in ten seconds from any Linux/Mac box on the same network — if this fills up almost instantly (normal mDNS traffic is a few packets a minute, not thousands in ten seconds), this is your problem:
sudo tcpdump -i <iface> -nn -e -c 3000 port 5353
Fastest fix — no rooting, about two minutes
The Fire TV is only reacting to a bad answer from somewhere else on the network. Fix that, and the flood stops on its own:
- Turn off (or remove from mDNS duty) whatever device is answering with the empty packet. In our case that was a FRITZ!Box used only as a Wi-Fi repeater, easily replaced. This alone dropped the flood from ~300 pps to ~1 pps. The Fire TV doesn't save the hostname anywhere — it only ever learns it live from that device's own mDNS announcements, so no announcements means nothing to chase.
- Can't remove that device? Run a small always-on responder that answers the Fire TV's query properly instead, with a standard negative reply ("this name has no AAAA record") followed by the correct A record. The Fire TV caches that and stops asking. Verified: 12 responses silenced 881 queries. It needs to keep running continuously — the negative-cache TTL is 120 seconds, so a one-off reply isn't enough — on any always-on Linux box on the LAN (a Raspberry Pi, a router running OpenWrt, anything). Python 3, standard library only, no dependencies:
#!/usr/bin/env python3
"""Answers a Fire TV's repeated mDNS AAAA query for a name with a proper
negative response (NSEC), instead of leaving it to an empty/bad reply that
Fire OS never backs off from.
Usage: mdns-nsec-responder.py <your-interface-ip> <name-being-queried> <that-name's-ip>
Example: mdns-nsec-responder.py 192.168.1.5 192-168-1-1.fritz.box 192.168.1.1
Find <name-being-queried> by watching tcpdump output above for the AAAA
query the Fire TV keeps repeating (it names the mDNS record it wants).
"""
import socket, struct, sys, time
IFACE_IP = sys.argv[1]
NAME = sys.argv[2].lower().rstrip(".")
NAME_IP = sys.argv[3]
TYPE_A, TYPE_AAAA, TYPE_NSEC = 1, 28, 47
CLASS_FLUSH_IN = 0x8001
MIN_INTERVAL = 0.25 # don't become the storm ourselves
TTL = 120
def encode_name(name):
out = b""
for label in name.split("."):
out += bytes([len(label)]) + label.encode()
return out + b"\x00"
def decode_name(data, off):
parts = []
while True:
ln = data[off]
if ln == 0:
off += 1
break
if ln & 0xC0 == 0xC0:
ptr = struct.unpack("!H", data[off:off + 2])[0] & 0x3FFF
parts.append(decode_name(data, ptr)[0])
off += 2
break
parts.append(data[off + 1:off + 1 + ln].decode("utf-8", "replace"))
off += 1 + ln
return ".".join(parts), off
def rr(name, rtype, rdata):
return encode_name(name) + struct.pack("!HHIH", rtype, CLASS_FLUSH_IN, TTL, len(rdata)) + rdata
def build_response(name, ip):
# NSEC bitmap: window 0, 1 byte, only the A-type bit set
# -> this name has an A record but no AAAA
answer = rr(name, TYPE_NSEC, encode_name(name) + bytes([0, 1, 0x40]))
additional = rr(name, TYPE_A, socket.inet_aton(ip))
return struct.pack("!HHHHHH", 0, 0x8400, 0, 1, 0, 1) + answer + additional
def main():
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
s.bind(("", 5353))
s.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, 255)
s.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_IF, socket.inet_aton(IFACE_IP))
s.setsockopt(socket.IPPROTO_IP, socket.IP_ADD_MEMBERSHIP,
socket.inet_aton("224.0.0.251") + socket.inet_aton(IFACE_IP))
s.settimeout(1.0)
response = build_response(NAME, NAME_IP)
last_sent = 0.0
while True:
try:
data, _ = s.recvfrom(9000)
except socket.timeout:
continue
except OSError:
time.sleep(1)
continue
try:
if struct.unpack("!H", data[4:6])[0] == 0: # no questions: it's a reply, ignore
continue
qname, off = decode_name(data, 12)
qtype = struct.unpack("!H", data[off:off + 2])[0]
except Exception:
continue
if qtype != TYPE_AAAA or qname.lower().rstrip(".") != NAME:
continue
now = time.time()
if now - last_sent < MIN_INTERVAL:
continue
last_sent = now
try:
s.sendto(response, ("224.0.0.251", 5353))
except OSError:
pass
if __name__ == "__main__":
main()
Run it in the foreground first to confirm it's working, then keep it running (a simple systemd unit is enough on most Linux boxes):
sudo python3 mdns-nsec-responder.py <your-lan-ip> <name-from-tcpdump> <that-name-ip>
# /etc/systemd/system/mdns-nsec-responder.service
[Unit]
Description=mDNS NSEC responder for the Fire TV storm bug
After=network.target
[Service]
ExecStart=/usr/bin/python3 /opt/mdns-nsec-responder.py <your-lan-ip> <name> <name-ip>
Restart=always
[Install]
WantedBy=multi-user.target
On OpenWrt, wrap it as a procd service instead — same script, started with
procd_set_param respawn so it survives crashes and reboots.
Neither of these two fixes touches the Fire TV itself. That's covered next, for anyone who wants the flood gone permanently rather than routed around.
Why this happens
The Fire TV Stick was repeatedly querying the AAAA record for a *.fritz.box
hostname — the self-assigned name a FRITZ!Box router announces over mDNS. The FRITZ!Box was
answering with empty mDNS packets:
192.168.1.1.5353 > 224.0.0.251.5353: 0* [0q] 0/0/0 (12)
Zero questions, zero records — not a valid answer, and not silence either. Fire OS doesn't treat that as a real response and applies no backoff: it retransmits at roughly 180 queries/second. Multicast frames are sent at the lowest common wireless rate to every client on the segment, so ~300 pps of multicast is enough to saturate 2.4GHz airtime for the whole network, while wired gigabit clients never notice.
This is a known, Amazon-acknowledged Fire OS bug, reported July 28, 2026, still unfixed as of this write-up.
A prediction that turned out wrong: Amazon's own report says the flood starts "when no response is available," which we initially read as meaning removing the FRITZ!Box would make things worse. It didn't — that line describes the loop after it starts, not how it's triggered. Without the FRITZ!Box announcing that hostname at all, the Fire TV never has anything to chase in the first place. Measuring beats guessing.
What we tried that didn't work
| Attempt | Result |
|---|---|
| Uninstalling apps from the Fire TV | No effect — it's a system component |
am force-stop / pm clear on com.amazon.whisperlink.core.android | Refused — protected package |
| Disabling IPv6 on the FRITZ!Box | Responses went from useless fe80:: to empty — same 300 pps |
| Rebooting the FRITZ!Box | Storm resumed immediately |
| Publishing the name via avahi | avahi doesn't answer for names outside .local |
| Filtering at the router | Traffic never crosses it — stays link-local between the Fire TV and the FRITZ!Box |
| NSEC responder without an A record | Not enough on its own — the trailing A record is required |
The two quick fixes, measured
| mDNS rate | Wi-Fi packet loss | |
|---|---|---|
| Baseline (storm active) | ~300 pps | 46–60% |
| Offending device powered off | ~1 pps | 0% |
| NSEC responder running continuously | ~0.1 pps | 0% |
Permanent fix — remove it from the Fire TV itself (advanced, requires temporary root)
Both mitigations above treat the symptom. The actual source is on the Fire TV: a component of Amazon's WhisperLink/WhisperPlay stack does the querying, and it's a protected system package — normal ADB can't touch it.
Device identification matters
There are two different "Fire TV Stick 4K Max" devices with very similar names:
karat(2nd gen, 2023, Fire OS 8.x) andkara(1st gen, 2021, Fire OS 7.x, MediaTek SoC). Public root tooling for one does not apply to the other — confirm the exact codename and build number withgetpropbefore assuming a guide applies.
Temporary root, then targeted removal
We used a public,
build-specific fork of the GhostLock kernel exploit (CVE-2026-43499), pinned to our
exact firmware (PS7713.5443 / build 0035334210436), to obtain
temporary root — it disappears on the next reboot, no bootloader unlock, no partition
writes. That temporary root was then used, directly, to remove just the three packages
responsible:
pm uninstall -k --user 0 com.amazon.whisperlink.core.android
pm uninstall -k --user 0 com.amazon.whisperplay.service.install
pm uninstall -k --user 0 com.amazon.whisperjoin.middleware.np
com.amazon.whisperplay.contracts was left in place deliberately, for
compatibility. Package removal is Android package-manager state, not a root artifact — it
survives reboots even though the root session that performed it doesn't.
What tripped us up along the way
- The kernel race exploit fails more often than it succeeds — that's expected, and the tooling deliberately does not auto-retry, since a device-wide exploit log can't be safely attributed to one invocation among several.
- The bundled step that switches the Home launcher to a third-party one failed its own read-back check twice in a row (a likely timing bug in that verification), triggering a clean automatic rollback both times — no side effects, but no forward progress either.
-
What actually worked: we didn't need the launcher swap at all. With root
already obtained and the daemon still alive on the device, we ran the three
pm uninstallcommands directly against it (wrapped inruncon u:r:shell:s0, since the raw root context can't reach the Android package-manager service on its own), skipping the launcher step entirely. -
After a partially-successful attempt, SELinux was left in
Permissivemode. The tooling refuses to start the exploit again until it reads backEnforcing— a clean reboot resets it.
Verifying the fix
sudo tcpdump -i <iface> -nn -c 3000 port 5353
| mDNS traffic (15s window) | |
|---|---|
| Before | ~4,500 packets, dominated by the Fire TV |
| After | 3 packets total, none from the Fire TV |
Neither the FRITZ!Box nor the NSEC responder are needed anymore. The query simply never gets generated in the first place.
Known caveat, not yet tested
Removing the WhisperLink stack may break device discovery for phone-to-TV
casting/screen-mirroring, and Alexa multi-room audio routing to this specific Stick. We kept
whisperplay.contracts in place for baseline compatibility, but haven't yet
exercised those specific features against the change.
For Amazon
This isn't a proper upstream fix — it's package removal on a temporarily-rooted device, not a patch to the closed-source WhisperLink binary, and it needs redoing after a factory reset. We don't have the WhisperLink source, so we can't send an actual diff. What we can send is the exact behavior that's wrong and the minimal fix for it:
on mDNS response received:
if response.questions == 0 and response.answers == 0:
# this is an empty reply, not "no reply" —
# today's code treats it as neither and just retries
treat_as(NXDOMAIN)
apply_negative_cache(ttl=120s)
stop_retransmitting()
else:
normal_backoff_logic()
The bug is specifically that an empty (zero questions, zero answers) mDNS packet doesn't get classified as a negative response, so the retry loop never triggers its own backoff. Backing off on empty replies the same way it already must for NSEC replies should be a small, self-contained change. We're linking this write-up from the existing Amazon Developer forum report in the hope it helps someone triage it.
How this was built, honestly
As with the other write-ups on this site, a significant part of the investigation notes and this text were put together with LLM assistance (Claude, through Claude Code). The packet captures, the physical access to the router and the Fire TV, and the decisions about what to actually run on the device were mine, under my authorisation for every root/write action taken against the hardware.
References
Amazon Developer forum report (2026-07-28)
Delitants/firetv-kara-standalone — kara-specific root tooling
R0rt1z2/GhostLock — CVE-2026-43499 kernel exploit (karat/sunstone)