When two segments claim the same sequence range but carry different bytes, TCP reassembly silently keeps the later one and still reports the datagram as complete. Nothing in the returned Datagram records that the two disagreed.
Reproduction, on c28ffc287
from pcapkit.foundation.reassembly.tcp import TCP
from pcapkit.foundation.reassembly.data.tcp import Packet
def seg(num, seq, payload, *, ack=1000):
return Packet(bufid=('192.0.2.1', 12345, '198.51.100.2', 443), dsn=seq, ack=ack,
num=num, syn=False, fin=False, rst=False, len=len(payload),
first=seq, last=seq + len(payload) - 1,
header=b'hdr', payload=bytearray(payload))
r = TCP(strict=True)
r(seg(1, 100, b'AAAAAAAA')) # original
r(seg(2, 100, b'BBBBBBBB')) # same range, different bytes
r(seg(3, 108, b'CCCC')) # stream continues
for d in r.fetch():
print(d.completed, bytes(d.payload), d.index)
True b'BBBBBBBBCCCC' (1, 2, 3)
b'AAAAAAAA' is gone without a trace, and completed is True.
Mechanism
pcapkit/foundation/reassembly/tcp.py:166:
else: # if fragment partially overlaps existing payload
RAW[PSN - ISN:PSN - ISN + info.len] = info.payload
The slice assignment is unconditional — the incoming payload is written over whatever occupied that range, with no comparison against the bytes already there. The hole-descriptor list is then updated from the segment's own bounds, so the overwritten range remains hole-free and completion is derived as COMPLETE. The mirrored branch at :176 (RAW = info.payload + RAW[-GAP:]) does the same for a segment that reaches back before the current ISN.
Why it is worth recording
A conforming stack retransmits identical bytes, so a disagreement means one of two things, and both matter to anyone using this for analysis:
- a broken or buggy sender, which the caller would want to know about;
- deliberately overlapping segments, which is the classic TCP-overlap IDS-evasion technique — the whole point of that attack is that different reassemblers resolve the conflict differently, so a tool that silently picks one and reports "complete" gives an answer that looks authoritative and isn't.
Last-write-wins is a defensible choice (it is roughly what Linux does), but making it silently and still claiming completeness is what turns it into a defect: there is no way for a caller to tell a clean stream from a contested one.
Possible directions
Not proposing a specific fix, since the right answer depends on whether the reassembler wants to grow a notion of "contested":
- compare before overwriting, and record the conflicting ranges on the
Datagram (a new field, so additive);
- surface it as a
ProtocolWarning when strict=True, leaving the resolution unchanged;
- keep first-write-wins instead, which is what some stacks do, and document the choice either way.
Found while reviewing #435, which changes completed to a Completion enum but does not touch this path — git diff origin/main over reassembly/tcp.py shows no change to either overlap branch, and :166/:176 are byte-identical on main. Filed separately rather than folded into that PR.
When two segments claim the same sequence range but carry different bytes, TCP reassembly silently keeps the later one and still reports the datagram as complete. Nothing in the returned
Datagramrecords that the two disagreed.Reproduction, on
c28ffc287b'AAAAAAAA'is gone without a trace, andcompletedisTrue.Mechanism
pcapkit/foundation/reassembly/tcp.py:166:The slice assignment is unconditional — the incoming payload is written over whatever occupied that range, with no comparison against the bytes already there. The hole-descriptor list is then updated from the segment's own bounds, so the overwritten range remains hole-free and
completionis derived asCOMPLETE. The mirrored branch at:176(RAW = info.payload + RAW[-GAP:]) does the same for a segment that reaches back before the current ISN.Why it is worth recording
A conforming stack retransmits identical bytes, so a disagreement means one of two things, and both matter to anyone using this for analysis:
Last-write-wins is a defensible choice (it is roughly what Linux does), but making it silently and still claiming completeness is what turns it into a defect: there is no way for a caller to tell a clean stream from a contested one.
Possible directions
Not proposing a specific fix, since the right answer depends on whether the reassembler wants to grow a notion of "contested":
Datagram(a new field, so additive);ProtocolWarningwhenstrict=True, leaving the resolution unchanged;Found while reviewing #435, which changes
completedto aCompletionenum but does not touch this path —git diff origin/mainoverreassembly/tcp.pyshows no change to either overlap branch, and:166/:176are byte-identical onmain. Filed separately rather than folded into that PR.