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:
puresnmp accepts a raw constructed Sequence as a varbind value rather than restricting to SNMP's ObjectSyntax choice.
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 side — src/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 side — x690/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.
An SNMPv2c response whose varbind value is a deeply-nested constructed
SEQUENCEcausesPyWrapper.get(and the other Pythonic-API entrypoints) to crash withRecursionError. Two layered defects, both required for the crash:puresnmpaccepts a raw constructedSequenceas a varbind value rather than restricting to SNMP'sObjectSyntaxchoice.x690.types.Sequencedecodes and pythonizes constructed values without a recursion-depth bound.A single response with ~1200 nested
SEQUENCEheaders as a varbind value (encodes to ~4.7 KB, fits in one UDP datagram) is enough to hit Python's defaultsys.getrecursionlimit() = 1000. Cross-posting / referencing this in thex690repo too since the deeper fix lives there.Tested against:
exhuma/puresnmp@5310ab863f87a76703ce548167be121ac7bc9b73(develop, version2.0.1)exhuma/x690@5a6343a97cedfe0fbc17dfe5ee76eb7251c1f520(release tagv1.0.0post1, version1.0.0.post1)What happens
V2CMPM.decodewalks only the outer SNMPv2c envelope — it constructs the innerSequencefor 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 callsPyWrapper.get(...)(orwalk/bulkwalk/setthat returns a value), wheresrc/puresnmp/api/pythonic.py:70invokesraw_value.pythonize()on the stored value, entering the recursiveSequence.pythonizechain and exceeding the recursion limit.End-to-end flow:
Where the defects are
puresnmp side —
src/puresnmp/pdu.py:101-110:There's no check that
valueis a member of SNMP'sObjectSyntaxchoice (Integer,OctetString,ObjectIdentifier,IpAddress,Counter32,Gauge32,TimeTicks,Opaque,Counter64,Null, plus the no-such-instance / end-of-mib markers). A raw constructedSequenceis accepted verbatim.x690 side —
x690/types.py:598-617(Sequence.decode_raw):x690/types.py:647-651(Sequence.pythonize):Both paths recurse one Python frame per nesting level. No depth counter. A chain of N nested
SEQUENCEheaders 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); installspuresnmp 2.0.1+x690 1.0.0.post1at the pinned commits and runs the PoC.files/poc.py: self-contained PoC that usespuresnmp'ssenderhook 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 aBUG REPRODUCED/BUG NOT REPRODUCEDverdict block. Exit code follows the verdict.files/expected-output.txt— frozen capture from a prior run.Observed output (excerpt):
The depth-4 control passes through
PyWrapper.getcleanly. 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 SNMPObjectSyntaxtypes before storing. A raw constructedSequenceis not a legal varbind value and should be rejected at the protocol layer regardless of whetherx690later 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) andSequence.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'sRecursionErrorescape. This protects every caller of x690, not just puresnmp.Regression-test predicate (mirrors the PoC):
PyWrapper.getand return alist.puresnmp.exc.MalformedVarbindorx690.exc.NestedDepthExceeded), notRecursionError.