alosafuzz
ICS/OT protocol security research — fuzzing, scanning, and coordinated disclosure.

Fuzzing SNMPv3 where it actually lives

October 7, 2026 · Project: snmpv3fuzzer

SNMP is old, unglamorous, and everywhere. It runs the management plane of nearly every switch, router, printer, UPS, and a great deal of industrial and IoT gear — and SNMPv3 added a real security model (USM: users, engine IDs, HMAC authentication, DES/AES privacy) on top of a wire format that is still, underneath, hand-rolled ASN.1/BER parsing. That combination — ubiquitous, security-relevant, and parsing attacker-controlled bytes — is exactly what you want to fuzz. The problem is that the obvious target is already covered.

net-snmp is saturated. It is an active OSS-Fuzz project and ships in-tree harnesses directly on the surface I care about — snmp_parse_fuzzer, snmp_pdu_parse_fuzzer, and snmp_scoped_pdu_parse_fuzzer for the SNMPv3 scoped PDU. Re-fuzzing a parser that Google already fuzzes continuously is wasted effort. So I went looking for an SNMPv3 stack with the same reach but none of the attention, and found it in lwIP — the lightweight TCP/IP stack embedded in a vast number of microcontrollers and IoT SDKs. Its SNMPv3 agent (src/apps/snmp/) is not in OSS-Fuzz and ships no fuzz harness. And in May 2026, CVE-2026-8836 — a CVSS 9.8 stack overflow in its USM msgAuthenticationParameters handling — proved the surface was live, memory-unsafe, and unfuzzed.

This is a writeup of a coverage-guided campaign against that surface, pinned at the commit that fixed CVE-2026-8836; so I was sweeping the residual surface, not rediscovering the known bug. The headline is not a CVE. Across roughly 200 million executions under AddressSanitizer and UndefinedBehaviorSanitizer — plain parse, USM authentication, and the crypto-gated scoped PDU, with both a deterministic crypto shim and the real mbedTLS backend — I found no memory-safety defect. One low-severity undefined-behavior issue, reported and already public (lwIP bug #68758). The post-patch lwIP SNMPv3 parser held up. As with any honest “nothing broke” result, the interesting part is how you make that claim trustworthy.

The method, briefly

lwIP is open source, so I fuzzed it in-process and coverage-guided (libFuzzer + ASan/UBSan, clang), compiling the real agent with instrumentation and driving its real entry point — snmp_parse_inbound_frame(), the static function CVE-2026-8836 lived in — directly, with no sockets. The harness builds a pbuf from the fuzz bytes exactly as the UDP transport would and calls the parser. A few decisions did the work:

Coverage matrix

The point of a “nothing broke” result is the reach behind it.

Surface Crypto What was fuzzed Executions Result
Outer message + BER decode + USM header shim snmp_parse_inbound_frame, no user 87.2M held up (found F1)
USM auth-verify + key handling shim configured user → HMAC verify path 42.3M held up
Crypto-gated scoped PDU (authPriv) shim valid envelope → decrypt → scoped PDU 8.6M held up
USM auth-verify + key handling real mbedTLS real HMAC-SHA1/MD5 over the message 61.0M held up
Crypto-gated scoped PDU (authPriv) real mbedTLS real HMAC + real AES-CFB valid envelope 3.1M held up

Target pinned at lwIP 0c957ec0 (the CVE-2026-8836 patch commit); F1 re-confirmed on current master d08f4773. The exec-rate fell by an order of magnitude from phase 1 to phase 3 (≈290k → ≈13k/s) — the signature of each phase reaching deeper, heavier code: by phase 3 every iteration assembles an envelope, computes a real HMAC, AES-encrypts, and drives the full authenticate-decrypt-parse path.

A few things worth reading

Most of this was “held up fine,” so here are the parts I found genuinely interesting.

The one finding — and why I’m calling it low-severity out loud. Within the first few hundred executions, UBSan flagged a left shift of a negative value in snmp_asn1_dec_s32t, the BER signed-integer decoder (snmp_asn1.c:472). When a multi-byte INTEGER’s leading byte has its high bit set, the running value becomes negative, and the next *value << 8 is undefined behavior per the C standard. It is reachable without authentication from any SNMP message carrying such an integer — a request-id, an SNMPv3 msgID, engine boots or time — across v1, v2c, and v3 alike. That sounds alarming, and the honest framing matters: this is a UBSan finding, not a memory-safety bug. ASan reported nothing — no out-of-bounds read or write, no corruption. On every mainstream compiler the shift compiles to an arithmetic shift and the decoded value is correct. It is undefined behavior (CWE-758) and a one-line fix — do the shift in unsigned space — but it is a portability/correctness hardening, not the CVE-2026-8836 class, and I reported it that way. It is below the bar for a CVE, so I didn’t request one; it’s filed as lwIP Savannah bug #68758.

Making auth pass is the whole trick of phase 3. It is easy to write a phase-3 harness that looks like it fuzzes the crypto-gated parser but silently fails authentication on every input — in which case you’re just re-running phase 2 and calling it something fancier. The fix is to verify it. I checked a debug build: after parsing a valid seed, the request’s error status was clean and its security flags stayed authPriv — and lwIP forces the flags down to noAuthNoPriv the instant authentication or decryption fails. Flags preserved, error zero: the gate was genuinely passed. Without that check, a phase-3 “result” is worthless.

Non-fatal asserts are a judgment call, stated plainly. Turning lwIP’s LWIP_ASSERT into a no-op means the fuzzer runs past lwIP’s own bounds checks on malformed input, which is what lets ASan see whether a missing check is actually exploitable rather than merely asserted. The tradeoff is that I am no longer testing “does lwIP assert here”; I’m testing “if it didn’t, would memory be corrupted.” For a memory-safety sweep that’s the right call, but it’s a choice worth being explicit about, because it changes what the result means.

Disclosure status

Lessons

Closing

I set out to find a memory-safety bug in an unfuzzed SNMPv3 agent, on a surface a CVSS-9.8 CVE had just proven to be dangerous. I found one benign undefined-behavior issue and, otherwise, a parser that held up across every layer I could reach, with real crypto and without. That is a quieter result than a CVE, and I think it is worth publishing exactly as it is: the standard this kind of work should be held to is not how loud the finding is, but how honestly you can defend the claim — including the claim that nothing broke.

Found with snmpv3Fuzzer, an open-source coverage-guided SNMPv3 fuzz harness over lwIP’s SNMP parser. By Shad Malloy (alosafuzz) — alosafuzz@proton.me.