Skip to content

PyWrapper.get raises uncaught RecursionError on a deeply-nested SEQUENCE varbind value #129

@hibrian827

Description

@hibrian827

An SNMPv2c response whose varbind value is a deeply-nested constructed SEQUENCE causes PyWrapper.get (and the other Pythonic-API entrypoints) to crash with RecursionError. Two layered defects, both required for the crash:

  1. puresnmp accepts a raw constructed Sequence as a varbind value rather than restricting to SNMP's ObjectSyntax choice.
  2. x690.types.Sequence decodes and pythonizes constructed values without a recursion-depth bound.

A single response with ~1200 nested SEQUENCE headers as a varbind value (encodes to ~4.7 KB, fits in one UDP datagram) is enough to hit Python's default sys.getrecursionlimit() = 1000. Cross-posting / referencing this in the x690 repo too since the deeper fix lives there.

Tested against:

  • exhuma/puresnmp@5310ab863f87a76703ce548167be121ac7bc9b73 (develop, version 2.0.1)
  • exhuma/x690@5a6343a97cedfe0fbc17dfe5ee76eb7251c1f520 (release tag v1.0.0post1, version 1.0.0.post1)

What happens

V2CMPM.decode walks only the outer SNMPv2c envelope — it constructs the inner Sequence for the varbind value but doesn't pythonize it. So envelope decode of a deeply-nested response succeeds quietly. The crash is deferred to the moment the application calls PyWrapper.get(...) (or walk/bulkwalk/set that returns a value), where src/puresnmp/api/pythonic.py:70 invokes raw_value.pythonize() on the stored value, entering the recursive Sequence.pythonize chain and exceeding the recursion limit.

End-to-end flow:

remote UDP bytes
  -> src/puresnmp/api/raw.py:348      (sender return)
  -> src/puresnmp/api/raw.py:354      (mpm.decode)
  -> puresnmp_plugins/mpm/v2c.py:66   (decode SNMPv2c envelope as Sequence)
  -> src/puresnmp/pdu.py:101          (parse varbind list)
  -> src/puresnmp/pdu.py:110          (store nested Sequence as varbind value)
  -> src/puresnmp/api/raw.py:397      (return raw value to wrapper)
  -> src/puresnmp/api/pythonic.py:70  (raw_value.pythonize())
  -> x690.types.Sequence.pythonize    (recurses one frame per nesting level)
  -> RecursionError

Where the defects are

puresnmp sidesrc/puresnmp/pdu.py:101-110:

varbinds, _ = decode(data, start_index, enforce_type=Sequence)
...
output.append(VarBind(oid, value))   # value is whatever x690 returned

There's no check that value is a member of SNMP's ObjectSyntax choice (Integer, OctetString, ObjectIdentifier, IpAddress, Counter32, Gauge32, TimeTicks, Opaque, Counter64, Null, plus the no-such-instance / end-of-mib markers). A raw constructed Sequence is accepted verbatim.

x690 sidex690/types.py:598-617 (Sequence.decode_raw):

items: List[X690Type[Any]] = []
end = slc.stop
while data_index < end:
    item, data_index = decode(data, data_index)
    items.append(item)
return items, data_index

x690/types.py:647-651 (Sequence.pythonize):

def pythonize(self) -> List[Any]:
    return [obj.pythonize() for obj in self.value]

Both paths recurse one Python frame per nesting level. No depth counter. A chain of N nested SEQUENCE headers translates to N frames at decode time and another N frames at pythonize time.

Reproducer

poc.zip

  • run.sh : one-command Docker reproducer (python:3.9-slim); installs puresnmp 2.0.1 + x690 1.0.0.post1 at the pinned commits and runs the PoC.
  • files/poc.py : self-contained PoC that uses puresnmp's sender hook to inject a synthetic SNMPv2c response (no real network), walks four narrated steps (depth-4 raw decode, depth-4 PyWrapper.get, depth-1200 raw decode, depth-1200 PyWrapper.get), and prints a BUG REPRODUCED / BUG NOT REPRODUCED verdict block. Exit code follows the verdict.
  • files/expected-output.txt — frozen capture from a prior run.

Observed output (excerpt):

sys.getrecursionlimit() = 1000
raw_decode    depth=4    bytes=48   ok envelope=GetResponse varbind_value_type=Sequence  seconds=0.004
pywrapper_get depth=4    bytes=48   ok return_type=list                                  seconds=0.001
raw_decode    depth=1200 bytes=4679 ok envelope=GetResponse varbind_value_type=Sequence  seconds=0.000
pywrapper_get depth=1200 bytes=4679 exc=RecursionError msg=maximum recursion depth exceeded seconds=0.005

The depth-4 control passes through PyWrapper.get cleanly. The depth-1200 case decodes the envelope without complaint, then crashes when the wrapper pythonizes the stored value.

Suggested fix

Two layers, both warranted:

puresnmp — at src/puresnmp/pdu.py:110, validate that each parsed varbind value is one of the legal SNMP ObjectSyntax types before storing. A raw constructed Sequence is not a legal varbind value and should be rejected at the protocol layer regardless of whether x690 later bounds its recursion. This kills the bug entirely for SNMP callers.

x690 — add a depth bound to constructed-value handling in both Sequence.decode_raw (x690/types.py:598-617) and Sequence.pythonize (x690/types.py:647-651). When depth exceeds a configured maximum (e.g. 32, matching what most BER decoders use), raise a bounded library exception (e.g. x690.exc.NestedDepthExceeded) instead of letting Python's RecursionError escape. This protects every caller of x690, not just puresnmp.

Regression-test predicate (mirrors the PoC):

  • The depth-4 fixture continues to decode through PyWrapper.get and return a list.
  • The depth-1200 fixture fails with a bounded library exception (e.g. puresnmp.exc.MalformedVarbind or x690.exc.NestedDepthExceeded), not RecursionError.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions