One Byte Every 30 Seconds: Permanently Killing epmd with CVE-2026-42792
This post walks through CVE-2026-42792, a chain of three small defects in Erlang's epmd (Port Mapper Daemon) that combine into a permanent, unauthenticated remote denial of service. Synack researcher Ryan Moore shows how an attacker sending about 34 bytes per second from a single IP can exhaust epmd's connection slots, trigger an unhandled file-descriptor error that kills the daemon, and exploit systemd's default restart-rate limits to permanently disable it, requiring manual operator recovery.
Technical Summary
Exploits Explained
CVE-2026-42792 · GHSA-h6f3-hx58-xhj6 · Patched in OTP 27.3.4.15, 28.5.0.4, and 29.0.4 · An unauthenticated attacker can hold open epmd's connection slots by sending one byte every 30 seconds, then force a file-descriptor exhaustion error that epmd's accept() loop mishandles as fatal, crashing the daemon.
Because epmd is socket-activated, systemd restarts it automatically, but the attacker's reconnect loop kills each fresh instance in about a second. After five crashes in ten seconds, systemd's default rate limiter gives up and latches both epmd.service and epmd.socket into a failed state.
The result is a permanent, unauthenticated remote denial of service, reached from first packet to failed state in about two seconds, that doesn't self-heal and requires manual operator recovery.
When you hear “denial of service,” you probably picture a botnet—thousands of hosts, gigabits of traffic, someone’s autoscaler melting. So the first thing I want you to sit with is this: the outage I’m about to describe costs an attacker about 34 bytes per second, from a single IP address, with no authentication. Roughly two seconds after the first packet, the target service is dead. And it doesn’t come back. Not when the traffic stops, not when systemd tries to restart it—not until a human logs in and runs a recovery command by hand.
That target is epmd, the Erlang Port Mapper Daemon. If you’ve never heard of it, that’s exactly why it’s interesting. It’s the quiet piece of plumbing sitting under RabbitMQ, CouchDB, ejabberd, and just about any application built on distributed Erlang or Elixir. Knock it over and every one of those nodes loses the ability to find its peers.
This is CVE-2026-42792. Here’s how three individually-boring defects stack into a remote, permanent kill switch.
What Is epmd, and Why Should You Care?
Distributed Erlang works by name. A node registers itself—”I’m rabbit@host, reachable on TCP port 47214″—and other nodes ask a central registry where to find it. That registry is epmd, and it listens on TCP 4369.
You almost never think about it because it’s supposed to be invisible. Install erlang-base on Debian or Ubuntu and the package quietly ships a systemd socket unit that binds 4369 on all interfaces—0.0.0.0 and [::], no -address flag, no loopback restriction. If that box is reachable from the network, so is epmd. And epmd speaks a simple, unauthenticated protocol: anyone who can open a TCP connection can talk to it.
That’s the target. Now the three bugs.
Bug 1: A Slot You Can Hold Forever
epmd tracks each client connection in a fixed-size slot table. My first question was the obvious one: can I just hold those slots open?
It turns out epmd wants to reap idle connections, but the logic has a gap. Every time a connection produces a successful read(), epmd refreshes that connection’s timestamp:
/* epmd_srv.c—on any successful read */
s->mod_time = current_time(g); And the idle-close only fires when the socket has been silent and the timestamp has aged past packet_timeout:
/* epmd_srv.c—the idle reaper */
if (!FD_ISSET(s->fd, &read_mask) &&
s->mod_time + g->packet_timeout < now) {
/* ... close the idle connection ... */
} Read those together and the flaw pops out: there is no absolute connection deadline. The timeout is measured from the last byte received, and any byte resets it. So if I send a single byte just before packet_timeout expires—every 30 seconds is comfortably inside the 60-second default—the slot never ages out. I can hold it indefinitely. There’s also no per-source-IP cap, so one attacker can hold all the slots.
There’s a small subtlety that makes this clean. epmd reads a two-byte length prefix, then waits for that many payload bytes. I send a prefix declaring a large packet and then drip the payload one byte at a time. The request never completes, epmd keeps waiting, and each byte refreshes mod_time:
# epmd_dos.py—the drip loop, one worker per connection
payload = bytes([LEN_HI, LEN_LO]) + b"A" * 3055 # length prefix says "3057 bytes coming"
for byte in payload:
s.sendall(bytes([byte]))
# sleep just under packet_timeout so mod_time keeps refreshing
select.select([s], [], [], DRIP_INTERVAL) # ...but wake early if the server closes At one byte every 30 seconds, a single connection would take ~47 hours to finish sending—long enough that “forever” is a fair description. On its own, this is a nuisance: I can exhaust the slot table and epmd will refuse new registrations. Annoying, but epmd is still running. The real damage needs bug two.
Bug 2: The Daemon That Quits on a Survivable Error
Here’s the one that got the CVE. When epmd’s accept loop can’t accept a connection, it inspects errno to decide whether to keep going:
/* epmd_srv.c—do_accept() */
switch (errno) {
case EAGAIN:
case ECONNABORTED:
case EINTR:
/* transient—ignore and continue */
break;
default:
epmd_cleanup_exit(g, 1); /* <-- everything else: die */
} Look at what falls into that default branch. EMFILE—”this process has hit its file-descriptor limit.” ENFILE—”the whole system is out of file descriptors.” These are textbook recoverable conditions. The correct response is to drop the one connection you couldn’t accept and keep serving the ones you already have. Instead, epmd calls epmd_cleanup_exit() and terminates the entire daemon.
This is CWE-755, improper handling of an exceptional condition, and it’s the hinge the whole attack turns on. An attacker doesn’t need a memory-corruption primitive or a logic flaw in the protocol. They just need to make accept() return EMFILE—and bug one is exactly the tool for that. Hold every file descriptor epmd is allowed to have, and the very next connection attempt pushes accept() into the error branch that kills the process.
Bug 3: How The OS Turns a Crash Into an Outage
A crashing daemon isn’t automatically a lasting outage. Modern init systems restart things. So why doesn’t epmd just come back? Because the Debian packaging composes with systemd’s defaults in a genuinely unfortunate way.
Three numbers line up. epmd sizes its slot table as min(sysconf(_SC_OPEN_MAX), FD_SETSIZE). On a default Debian install, systemd hands the service DefaultLimitNOFILESoft = 1024, and FD_SETSIZE is also 1024. So the slot table size equals the file-descriptor limit. Exhausting slots (bug one) and exhausting file descriptors (which triggers bug two) become the same event. Fill the table and you’ve simultaneously armed the crash.
Then watch the restart dynamics. The shipped epmd.service sets no Restart= and no LimitNOFILE= override. epmd is socket-activated, so when it dies, systemd dutifully spins up a fresh instance the next time a client connects—but my worker threads notice the dropped connection (that’s the select() in the drip loop) and reconnect within milliseconds, killing the newborn epmd in about a second. It’s socket-activation whack-a-mole.
systemd only tolerates that for so long. Its default start-rate-limiter allows DefaultStartLimitBurst = 5 starts within DefaultStartLimitIntervalSec = 10 seconds. On the fifth death, systemd gives up and latches both epmd.service and epmd.socket into the failed state with reason service-start-limit-hit. The kernel listen socket on 4369 is released. Every subsequent client—attacker or legitimate—gets Connection refused.
That’s the transition that matters: a self-healing crash becomes a permanent outage. And crucially, systemd will not clear that latched failure on its own.
Putting It Together: The Demo
Chained end to end against a default Debian VM, it looks like this:
- Open ~1,024 connections; drip one byte per connection every 30 seconds.
- epmd accepts up to ~1,020 clients, then
accept()returnsEMFILE. - Bug 2 fires →
epmd_cleanup_exit()→ the daemon dies. - systemd socket-activates a fresh epmd on the next connect.
- Workers detect the close and reconnect within milliseconds.
- The new epmd dies in ~1 second—same path.
- After 5 deaths in 10 seconds, systemd marks the service and the socket
failed. - The listen socket on 4369 is released; everything gets
Connection refused.
Total time from first packet to permanent outage: about two seconds. A separate verifier probing epmd as a normal client tells the story from the outside:
HH:MM:SS OK 0.00s 54 bytes <- healthy baseline
HH:MM:SS CONNECT-FAIL 0.00s [Errno 111] Connection refused
HH:MM:SS CONNECT-FAIL 0.00s [Errno 111] Connection refused On the victim, epmd -names now returns epmd: Cannot connect to local epmd, and systemctl status epmd.socket reads Active: failed (service-start-limit-hit). Any live Erlang node has already lost its registry entry and can’t re-register. Recovery requires an operator to run systemctl reset-failed epmd.service epmd.socket && systemctl start epmd.socket—by hand.
Why The Obvious Fix Isn’t One
Erlang’s docs offer -packet_timeout N (default 60s) to shrink the idle window, and it’s tempting to reach for it. It doesn’t help. Lower the timeout to 5 seconds and the attacker simply drips every 4 seconds instead of every 30—same hold, same exhaustion, same crash. Tuning the timeout changes the attacker’s cadence and nothing else. It does not touch bug two, which is where the outage actually comes from.
Remediation
Patch. The upstream fix teaches do_accept to treat EMFILE/ENFILE as recoverable. Fixed in OTP 27.3.4.15, 28.5.0.4, and 29.0.4 (erts 15.2.7.11, 16.4.0.4, 17.0.4).
Don’t expose 4369. epmd almost never needs to be internet-facing. Pin it to loopback with a systemd socket override:
# /etc/systemd/system/epmd.socket.d/override.conf
[Socket]
ListenStream=
ListenStream=127.0.0.1:4369
ListenStream=[::1]:4369 Then systemctl daemon-reload && systemctl restart epmd.socket epmd.service. Real Erlang distribution between hosts should ride TLS distribution (inet_tls_dist) on its own port, not naked epmd. Firewall 4369 to trusted hosts regardless.
Harden the unit. Setting a generous LimitNOFILE= and a Restart= policy blunts the systemd-rate-limit latch even before you patch.
Exploit Lab Demo
Using a fresh Debian virtual machine

Erlang-base is installed

The epmd service is running on all interfaces, port 4369 by default

A Python script is used to demonstrate the exploit for this vulnerability.

Journalctl logs show the epmd service has failed.

Netstat confirms, the service is no longer listening and has permanently crashed.

Disclosure and Takeaway
- CVE-2026-42792 · GHSA-h6f3-hx58-xhj6
- CVSS: 7.5 HIGH (NIST, CVSS 3.1:
AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H)—the Erlang/OTP CNA scored it 6.3 MEDIUM under CVSS 4.0 - CWE-755 (Improper Handling of Exceptional Conditions)
- CWE-770 (Allocation of Resources Without Limits)
- Affected: Erlang/OTP ≥ 17.0 (erts ≥ 6.0)
- Reported by: Ryan Moore
- Published: July 27, 2026
Disclosure Timeline
- 5/1/2026 – Reported to vendor via Github
- 5/4/2026 – CVE-2026-42792 id provided by vendor
- 7/20/2026 – Vendor releases patch
- 7/23/2026 – CVE information made publicly available
- 8/25/2026 – Debian releases DSA 6464-1
- 9/28/2026 – Ubuntu releases USN-8827-1
Thanks for reading. To learn more about the global community of researchers uncovering vulnerabilities like this, check out the Synack Red Team and apply today. Be sure to follow Synack and the Synack Red Team on LinkedIn for upcoming blogs in the Exploits Explained series.
Frequently Asked Questions
epmd (the Erlang Port Mapper Daemon) is the registry that lets distributed Erlang and Elixir nodes find each other, and it runs underneath applications like RabbitMQ, CouchDB, and ejabberd. It listens on TCP port 4369 with no authentication, and on a default Debian install, it binds to all network interfaces rather than just localhost. Knocking it offline breaks node discovery for every application built on top of it.
The exploit chains three separate bugs. An attacker first holds every available connection slot open by sending one byte every 30 seconds, which resets epmd’s idle timeout without ever completing a request. That exhausts epmd’s file descriptors, which pushes its accept() call into an error-handling path that terminates the entire daemon rather than just dropping the one connection. Because Debian’s default systemd restart limits allow only 5 restarts in 10 seconds, and the attacker’s reconnect loop retriggers the crash within milliseconds each time, systemd gives up and marks the service permanently failed.
No. Shortening the timeout only forces the attacker to send bytes more frequently. It doesn’t address the real issue, which is that epmd’s accept() error handling treats a recoverable, survivable condition like a file-descriptor limit as fatal. The real fix is the upstream patch (OTP 27.3.4.15, 28.5.0.4, or 29.0.4), combined with keeping epmd off the public internet and pinned to loopback.
What would 1,500
elite hackers find in
your stack?
The Synack Red Team and Sara AI Pentesting work side by side to test your environment and find vulnerabilities that matter.


