PFB-012Vendor: Prusa ResearchProduct: Buddy firmware
An unauthenticated host that reaches the printer over TCP pins the entire Ethernet receive pool and stalls all networking
- Severity
- CVSS 7.5High
CVSS:3.1/, FIRST CVSS calculator (opens in a new tab)AV:N/ AC:L/ PR:N/ UI:N/ S:U/ C:N/ I:N/ A:H CVSS metrics and CWE
- Attack Vector: Network (worst case)
- Attack Complexity: Low (worst case)
- Privileges Required: None (worst case)
- User Interaction: None (worst case)
- Scope: Unchanged
- Confidentiality: None
- Integrity: None
- Availability: High (worst case)
- CVE
- Not yet assigned
- Affected models
- CORE One L+
- CORE One L
- CORE One+ (Gen 2) INDX
- CORE One INDX
- CORE One+ (Gen 2)
- CORE One
- XL+
- XL
- MK4S
- MK4
- MK3.9S
- MK3.9
- MK3.5S
- MK3.5
- MINI
- Fixed in
- 7.0.0
- Attack surface
- Any host that can reach the printer's TCP services
- Verification
- Source review only
source-verified. Not run against hardware.
- Analysis notes
Versions and source tree
Prusa Buddy firmware, MK4S. The analysis used the 6.6.0 source tree (
version.txt6.6.0, commit e96ce2b9). Our test printer runs 6.5.7. Every line cited below is from the 6.6.0 tree.- Disclosed in
- Never gonna print you up!
Summary
The firmware allocates every inbound Ethernet frame from one small ring, rx_allocator_buffer (lib/WUI/pbuf_rx.cpp:17).
The ring is sized for ten 1024-byte buffers plus overhead (pbuf_rx.cpp:9-15), and seven full-size 1514-byte frames leave no room for an eighth.
Two independent mechanisms let any host that reaches the printer over TCP hold buffers from that ring.
Each mechanism has a timeout that should limit the hold, and in each one the attacker resets that timeout by continuing to send.
Neither mechanism needs the PrusaLink password.
When no free buffer remains in the ring, the driver has nowhere to put inbound frames.
PrusaLink, Prusa Connect and mDNS then all stop for as long as the attacker keeps sending.
The out-of-order pinning here and the byte-residency primitive in PFB-008 are the same defect, seen from two sides.
This advisory covers the denial of service on its own.
The defect
Two defects reach the same ring. Each removes the bound that should release a held receive buffer.
Mechanism 1: the out-of-order queue cap is per connection. The firmware caps the out-of-order queue at three retained buffers. This cap reads as a global limit:
// include/buddy/lwipopts.h:123
#define TCP_OOSEQ_MAX_PBUFS 3lwIP enforces this cap per pcb, not globally.
lwip/opt.h:1382-1383 expands it to TCP_OOSEQ_PBUFS_LIMIT(pcb), and tcp_in.c:1832-1846 enforces it by walking one pcb's own ooseq chain.
The queued segments are references to the original receive pbufs, not copies.
tcp_in.c:1650 stores the segment through tcp_seg_copy.
That helper says "the pbuf and data are not copied, only the pointers", and then it calls pbuf_ref on the shared pbuf (tcp.c:1655-1673).
Those pbufs come from the receive ring, so every held out-of-order segment keeps a ring slot allocated.
The connection-slot count sets the maximum number of these per-pcb holds:
// include/buddy/lwipopts.h:137
#define MEMP_NUM_TCP_PCB 12tcp_seg_copy allocates each queued segment from lwIP's shared pool of TCP segments (tcp.c:1668).
lwipopts.h does not set the size of that pool, so it keeps lwIP's default of 16 (lwip/opt.h:451-452).
The pool caps the total hold at 16 segments.
This is more than twice the seven full-size frames the ring holds.
The purge that should release these buffers (tcp.c:1354-1357) runs only when a pcb stays idle for longer than a timeout.
lwIP resets the idle marker (pcb->tmr) to tcp_ticks on every inbound segment (tcp_in.c:765, tcp_in.c:845).
A sender that continues to send a few bytes at a time therefore never lets the purge run.
Mechanism 2: the HTTP server re-arms the activity timer before it refuses the buffer.
In recv_wrap, the server acts in this order:
- It marks activity.
- It decides whether it can take the pbuf.
- If it cannot take the pbuf, it returns
ERR_MEMand does not free the pbuf (server.cpp:588-589).
// lib/WUI/nhttp/server.cpp:574-590
slot->server->activity(conn, slot);
if (slot->take_pbuf(data)) {
...
return ERR_OK;
} else {
// No freeing of pbuf!
return ERR_MEM;
}The activity call at server.cpp:574 runs before the take-or-refuse decision at server.cpp:576, so a refused buffer still counts as progress.
activity re-arms the slot's timeout (server.cpp:651):
// lib/WUI/nhttp/server.cpp:649-651
void Server::activity(altcp_pcb *conn, BaseSlot *slot){
if (is_active_slot(slot)) {
slot->timeout.schedule(ACTIVE_TIMEOUT);schedule resets last_activity to the current time (server.cpp:22-23).
ACTIVE_TIMEOUT is ten seconds (server.h:168).
lwIP keeps the un-taken data in pcb->refused_data (tcp_in.c:518) and re-delivers it on every fast-timer tick (tcp.c:1509-1511).
Each redelivery runs activity again and re-arms the timeout, so a slot that already holds a partial buffer never expires.
Only three active slots exist, ACTIVE_CONNS (server.h:155).
They share two send buffers, BUFF_CNT (server.h:157).
The step loop refuses to consume new input while an output buffer still waits for an ack (server.cpp:262-279).
A stalled response therefore pins its slot for the whole cycle.
Reaching it
The attacker is any host that can open TCP connections to the printer's port 80 (lib/WUI/http_lifetime.cpp:66,79).
A host on the same LAN segment is the common case.
Mechanism 1 is pure TCP and never reaches the HTTP layer. It has three steps:
- Complete the handshake on up to twelve connections.
- Send a segment that leaves a hole in the sequence space, so that lwIP queues it on
ooseq. - Keep each connection active with an occasional in-window byte.
The HTTP selectors check authentication long after the stack takes these buffers. Authentication therefore does not stop the attack.
Mechanism 2 needs only an HTTP connection whose response stalls. One example is a peer that advertises a zero receive window, so the server's output stays unacked.
Impact
The denial of service covers the printer's whole network stack.
When no free buffer remains in the ring, pbuf_alloc_rx returns null and the driver drops the frame (lib/WUI/ethernetif.c:351).
The drop is at ethernetif.cpp:12 for wired and at espif.cpp:761,767 for Wi-Fi.
Inbound traffic to every service therefore stops.
The denial of service persists as long as the attacker sends.
The defect does not disclose or corrupt memory. The impact is on availability only.
Proof
This finding comes from the source only. We did not run it against hardware. We read every constant and code path above in the 6.6.0 tree. This advisory leaves out the exact packet pattern that pins the ring. Prusa received it with the report.
Scoring note
We scored AV:N because both mechanisms use routable TCP.
Source release
Prusa published the 7.0.0 sources under the v7.0.0 release tag on GitHub.