Skip to content

TCP reassembly silently resolves conflicting retransmissions last-write-wins and still reports COMPLETE #443

Description

@JarryShaw

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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugIssues reporting a defect (set by the bug report template; a default, not an assessment)

    Projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions