Skip to content

OptionField.unpack never returns for a well-formed HOPOPT header with an SMF_DPD option #431

Description

@JarryShaw

OptionField.unpack never returns for a well-formed HOPOPT header. Confirmed on
main (31b08b0ac) — 15 seconds of CPU with no return, interrupted by an alarm:

from pcapkit.protocols.internet.hopopt import HOPOPT
buf = bytes.fromhex('3b00' '0803810203' '00')   # next=59, len=0; SMF_DPD len=3 + one Pad1
HOPOPT(buf, len(buf))                            # never returns

Eight octets. Nothing malformed about it: len=0 means an 8-octet header, and the
6-octet option area holds an SMF_DPD option of length 3 followed by one Pad1.

Mechanism

Instrumenting the loop body:

step 0: length=6  code=SMF_DPD  schema=SMFHashBasedDPDOption  len(data)=5  stream advanced 6  -> length 1
step 1: length=1  code=Pad1     schema=PadOption             len(data)=0  stream advanced 0  -> length 1
step 2..n: identical, forever

The loop decrements its remaining length by len(data), but len(data) is not the
number of octets consumed. On step 0 the unpack advanced the stream 6 octets while
reporting len(data) == 5, so length fell to 1 with the stream already exhausted.
From then on every iteration reads nothing, len(data) is 0, and length -= 0 makes
no progress.

The accounting mismatch comes from _SMFDPDOption.post_process returning the nested
schema, so len(data) measures the nested schema rather than the octets the wrapper
consumed. Why the wrapper over-reads by exactly one octet is a layer deeper, in
_SMFDPDOption's test-bit offsets, and is not traced here.

Note this rules out the obvious substitute fix: a file.tell() delta is not
equivalent to len(data) for the wrapper schemas whose post_process returns a nested
schema, which is the same root cause behind the len(data)/len(meta) accounting noted
on #427.

Why it matters

pcapkit is a parser fed untrusted input. A remote peer that can put an SMF_DPD option
in a Hop-by-Hop header can hang the process — no exception, no progress, no diagnostic.
Of everything surfaced in this round of work, this is the one I would fix first.

Suggested shape

The loop needs a progress guarantee independent of len(data) — the octets actually
consumed from the stream, or a check that each iteration advances and a
ProtocolError if it does not. The _SMFDPDOption over-read is a separate defect
underneath it and wants its own fix; a progress guard would turn the hang into a
diagnosable error either way.

Found while addressing review on #427. The mechanism first reported there — a lone
trailing Pad1 — was wrong: bytes([59, 0]) + bytes(6) parses fine. The bytes above
are the actual reproducer.

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