What happens
One external TFTP read request leaves Scapy's read-server automaton transmitting the same data
block every three seconds until it receives a matching acknowledgement. There is no retry count or
terminal timeout, so a requester that sends no ACK keeps the server instance occupied indefinitely
and prevents it from accepting another request.
Background — what this code does
Trivial File Transfer Protocol (TFTP) sends a file as numbered data blocks over UDP. The receiver
acknowledges each block, and the sender retransmits a block after a timeout when its
acknowledgement may have been lost.
TFTP_RRQ_server is Scapy's answering automaton for read requests. An application explicitly
starts it with an in-memory file store or a served directory. The automaton receives an RRQ, sends the
first data block, and waits for the corresponding ACK before moving to the next block or returning to
request handling.
How the code is reached
An unauthenticated requester sends an ordinary, wire-sized RRQ for a filename present in the
configured store or directory. No malformed packet or changed Scapy configuration is needed. If
the requester sends no ACK, the ordinary loss-recovery timeout repeatedly enters the data-sending
state.
This is response generation caused by an external stimulus: the behavior occurs in an answering
automaton after receiving the request, rather than while an operator crafts a packet directly.
Why it matters
The automaton handles one transfer at a time. While it waits for the missing ACK, later RRQs do
not reach the request-handling state. The server also continues sending one data packet every three
seconds without receiving any further packet from the requester.
The reproduced 47-byte RRQ caused block 1 to be sent at 0, 3, 6, 9, and 12 seconds. The automaton
was still active when the bounded observation ended. The same request followed by ACK 1 sent one
data packet and completed.
Reproduce it
The following program uses an in-process socket and the real three-second automaton timer. It
needs no live capture or privileges.
import time
from scapy.automaton import select_objects
from scapy.layers.inet import IP, UDP
from scapy.layers.tftp import TFTP, TFTP_ACK, TFTP_DATA, TFTP_RRQ, TFTP_RRQ_server
def request():
return (
IP(src="198.51.100.66", dst="192.0.2.10")
/ UDP(sport=40000, dport=0x2807)
/ TFTP()
/ TFTP_RRQ(filename="served.txt")
)
def ack():
return (
IP(src="198.51.100.66", dst="192.0.2.10")
/ UDP(sport=40000, dport=0x2807)
/ TFTP()
/ TFTP_ACK(block=1)
)
def run(send_ack):
class WireSocket:
packets = [request()] + ([ack()] if send_ack else [])
sent = []
def __init__(self, iface):
self.iface = iface
def recv(self, size=None):
return self.packets.pop(0)
def send(self, packet, *args, **kwargs):
self.sent.append((time.monotonic(), packet.copy()))
def close(self):
pass
@classmethod
def select(cls, inputs, remain):
sockets = [sock for sock in inputs if isinstance(sock, cls)]
if sockets:
if sockets[0].packets:
return sockets
inputs = [sock for sock in inputs if not isinstance(sock, cls)]
return select_objects(inputs, remain)
server = TFTP_RRQ_server(
ip="192.0.2.10",
sport=0x2807,
store={"served.txt": b"attacker-selected-response"},
serve_one=True,
ll=WireSocket,
recvsock=WireSocket,
)
server.runbg()
if send_ack:
deadline = time.monotonic() + 1
while server.isrunning() and time.monotonic() < deadline:
time.sleep(0.01)
else:
time.sleep(12.5)
running = server.isrunning()
if running:
server.forcestop()
server.destroy()
started = WireSocket.sent[0][0]
sends = [
(round(when - started, 2), packet[TFTP_DATA].block)
for when, packet in WireSocket.sent
]
return running, sends
print("no ACK:", run(False))
print("ACK control:", run(True))
Commit 1f870205baae8baf1718bc20700d0d9ffc0e4324 prints:
no ACK: (True, [(0.0, 1), (3.0, 1), (6.0, 1), (9.0, 1), (12.0, 1)])
ACK control: (False, [(0.0, 1)])
With the proposed fix, the no-ACK result becomes:
no ACK: (False, [(0.0, 1), (3.0, 1), (6.0, 1), (9.0, 1)])
Where it goes wrong
scapy/layers/tftp.py:524-533 sends the current data block and enters the same state on every timeout:
@ATMT.state()
def SEND_FILE(self):
self.send(
self.l3 / TFTP_DATA(block=self.blk % 65536) /
self.data[(self.blk - 1) * self.blksize:self.blk * self.blksize]
)
@ATMT.timeout(SEND_FILE, 3)
def timeout_waiting_ack(self):
raise self.SEND_FILE()
There is no counter or alternate timeout transition.
scapy/layers/tftp.py:535-553
shows that only a matching ACK advances the block and allows the transfer to finish:
if TFTP_ACK in pkt and pkt[TFTP_ACK].block == self.blk:
raise self.RECEIVED_ACK()
# ...
def RECEIVED_ACK(self):
self.blk += 1
Suggested fix
Allow three retransmissions for each data block. Reset the retry count when a new request arrives
and, on the first timeout for a new block, after the previous block's matching ACK. Keeping the
bookkeeping on the timeout path avoids adding work to every valid ACK.
After three retransmissions, a server created with serve_one=True terminates. A reusable server
returns to its request-waiting state. The initial data send and three loss-recovery attempts
remain available, while an acknowledged transfer is unchanged.
The attached patch adds a regression to the existing test/scapy/layers/tftp.uts suite. Its
socket helper is local to the test and checks both the no-ACK limit and an immediate-ACK control.
Removing the source change makes the test fail; applying it produces one passing focused test.
The configured BSD suite reported 291 passed and the same 3 existing interactive/startup failures
before and after the patch.
Performance impact of the fix, measured on one computer by running the same test before and after:
a matching ACK took 452.1 ns before and 425.9 ns after, 5.8% faster — a difference of 26 ns.
Repeat runs of that test moved by about 2%, so that difference is larger than the test's own
variation.
Affected
- Package: Scapy · Branch:
master
- Confirmed on: commit
1f870205baae8baf1718bc20700d0d9ffc0e4324,
version 2.7.1rc1.post100
- Severity: Medium —
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:L (5.3). The score reflects
unauthenticated network access and a low-rate loss of availability for one explicitly started
TFTP server instance, without demonstrated confidentiality or integrity impact.
- CWE: CWE-400, Uncontrolled Resource Consumption
Credit
Reported by: Clinton Thomas (@KernelClint) of Trail of Bits, in collaboration with OpenAI.
Found with GPT-5.6-Cyber as part of the Patch the Planet security initiative.
What happens
One external TFTP read request leaves Scapy's read-server automaton transmitting the same data
block every three seconds until it receives a matching acknowledgement. There is no retry count or
terminal timeout, so a requester that sends no ACK keeps the server instance occupied indefinitely
and prevents it from accepting another request.
Background — what this code does
Trivial File Transfer Protocol (TFTP) sends a file as numbered data blocks over UDP. The receiver
acknowledges each block, and the sender retransmits a block after a timeout when its
acknowledgement may have been lost.
TFTP_RRQ_serveris Scapy's answering automaton for read requests. An application explicitlystarts it with an in-memory file store or a served directory. The automaton receives an RRQ, sends the
first data block, and waits for the corresponding ACK before moving to the next block or returning to
request handling.
How the code is reached
An unauthenticated requester sends an ordinary, wire-sized RRQ for a filename present in the
configured store or directory. No malformed packet or changed Scapy configuration is needed. If
the requester sends no ACK, the ordinary loss-recovery timeout repeatedly enters the data-sending
state.
This is response generation caused by an external stimulus: the behavior occurs in an answering
automaton after receiving the request, rather than while an operator crafts a packet directly.
Why it matters
The automaton handles one transfer at a time. While it waits for the missing ACK, later RRQs do
not reach the request-handling state. The server also continues sending one data packet every three
seconds without receiving any further packet from the requester.
The reproduced 47-byte RRQ caused block 1 to be sent at 0, 3, 6, 9, and 12 seconds. The automaton
was still active when the bounded observation ended. The same request followed by ACK 1 sent one
data packet and completed.
Reproduce it
The following program uses an in-process socket and the real three-second automaton timer. It
needs no live capture or privileges.
Commit
1f870205baae8baf1718bc20700d0d9ffc0e4324prints:With the proposed fix, the no-ACK result becomes:
Where it goes wrong
scapy/layers/tftp.py:524-533sends the current data block and enters the same state on every timeout:There is no counter or alternate timeout transition.
scapy/layers/tftp.py:535-553shows that only a matching ACK advances the block and allows the transfer to finish:
Suggested fix
Allow three retransmissions for each data block. Reset the retry count when a new request arrives
and, on the first timeout for a new block, after the previous block's matching ACK. Keeping the
bookkeeping on the timeout path avoids adding work to every valid ACK.
After three retransmissions, a server created with
serve_one=Trueterminates. A reusable serverreturns to its request-waiting state. The initial data send and three loss-recovery attempts
remain available, while an acknowledged transfer is unchanged.
The attached patch adds a regression to the existing
test/scapy/layers/tftp.utssuite. Itssocket helper is local to the test and checks both the no-ACK limit and an immediate-ACK control.
Removing the source change makes the test fail; applying it produces one passing focused test.
The configured BSD suite reported 291 passed and the same 3 existing interactive/startup failures
before and after the patch.
Performance impact of the fix, measured on one computer by running the same test before and after:
a matching ACK took 452.1 ns before and 425.9 ns after, 5.8% faster — a difference of 26 ns.
Repeat runs of that test moved by about 2%, so that difference is larger than the test's own
variation.
Affected
master1f870205baae8baf1718bc20700d0d9ffc0e4324,version
2.7.1rc1.post100CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:L(5.3). The score reflectsunauthenticated network access and a low-rate loss of availability for one explicitly started
TFTP server instance, without demonstrated confidentiality or integrity impact.
Credit
Reported by: Clinton Thomas (@KernelClint) of Trail of Bits, in collaboration with OpenAI.
Found with GPT-5.6-Cyber as part of the Patch the Planet security initiative.