Skip to content

refactor: untangle the codec's read side - #54

Merged
ilyam8 merged 7 commits into
usm-decode-keysfrom
codec-reads
Oct 13, 2026
Merged

ilyam8 merged 7 commits into
usm-decode-keysfrom
codec-reads

Conversation

@ilyam8

@ilyam8 ilyam8 commented Oct 13, 2026 •

Copy link
Copy Markdown
Member

Summary

Phase 6 (guardrails), codec reads. No behavior change, no exported API change.

  • Header fields. Twenty header field reads each spelled seven lines (read, wrap the error as "error parsing
    ", assign when the field has the expected type). A small fieldReader keeps the first error, with the same
    text, and makes every later read a no-op, so each field is a three-line read-and-assign and a decoder checks the
    error once. Tried on unmarshalResponse first (which now takes the PDU type from its caller, and whose GetBulk/else
    branches hold two reads each), then used for the community, the SNMPv1 trap header, the SNMPv3 header, the scoped
    PDU and the USM parameters. readField is its one read; readInt, readString and readUint are gone, and the
    version and the varbind name, single reads each followed by its own check, call readField directly.
  • Varbinds. The varbind list loop's body is readVarbind, the read-side counterpart of appendVarbind; no reader
    or tag variable is shadowed any more.
  • Version. readVersion takes only the message. Its predecessor took a packet only to nil-check it (no caller
    passes nil) and to preset its variables, which unmarshalHeader now does itself under the same condition, marked as
    the known bug it is. unmarshalPayload loses the same unreachable nil check.
  • Trap table. UnmarshalTrap's table branch is unmarshalTrapByUser: one attempt per parameter set on a clone of
    the trap, with an explicit last error instead of one carried by a variable shadowing the named result.

Validation

  • New pins, each passing on the code before the refactor: every touched read failing in turn, with its exact error
    text and the partial packet SnmpDecodePacket returns (the decode golden records outcomes only); the trap table's
    attempts (a later set decodes after an earlier one failed, the last attempt's error is reported, the caller's trap
    is untouched); a table receiver decoding SNMPv1 and SNMPv2c traps; mistyped header fields keeping the value already
    there (the USM parameters start as a copy of the decoder's).
  • Codec, engine, FIPS 140-only and USM goldens unchanged.
  • 39 mutation probes on the moved and rewritten code: 36 killed (the new pins caught three: a table receiver decoding
    SNMPv1/SNMPv2c traps, and mistyped header fields keeping the value already there); 3 cannot change any reachable
    outcome.
  • A/B (8 interleaved rounds against the previous PR's head): allocations unchanged (SNMPv3 trap decoding one fewer);
    time +1-3% on larger messages and up to +9% (+28 ns) on the smallest SNMPv3 one, about 2.3 ns per header field read:
    a reader that skips after a failure and keeps the error does not fit Go's inlining budget. Accepted for the
    readability. A first form of the reader kept the reader beside its error, and escape analysis, which tracks a struct
    as one value, moved every decoder's reader to the heap (+2 allocations per message); fixed here.
  • go test -race, FIPS 140-only, golangci-lint, cross-builds, govulncheck: pass locally.

- A field of each decoder fails in turn: the exact error text, and the
  packet SnmpDecodePacket returns with it, whose fields read before the
  failure are set and the later ones are not. The variables are an
  empty list, not nil, for any input of two octets or more, decoded or
  not (known bug). The decode golden records outcomes, not error texts
  or partial packets.
- The trap table path tries a user's parameter sets in the order they
  were added, each on its own copy of the trap: a later set decodes
  after an earlier one failed, and when none does the error carries the
  last attempt's.
… own loop

- readVersion takes only the message: unmarshalVersionFromHeader took a
  packet to nil-check it (no caller passes nil) and to preset its
  variables, which unmarshalHeader now does itself, under the same
  condition, marked as the known bug it is. UnmarshalTrap and the
  engine harness no longer allocate a throwaway packet for it.
- unmarshalPayload loses the same unreachable nil-packet check.
- UnmarshalTrap's table branch is unmarshalTrapByUser: one attempt per
  parameter set on bytes.Clone of the trap, and an explicit last error
  instead of one carried by a variable shadowing the named result.
unmarshalVBL's loop body reads one varbind; it is readVarbind now, the
read-side counterpart of appendVarbind. The list's own header is
checked on the slice, so no reader is shadowed by the varbind's and no
tag variable is declared three times. Varbinds decoded before an error
stay appended, as before.
Twenty header field reads each spelled seven lines: read, wrap the
error as "error parsing <field>", assign when the field has the
expected type. A fieldReader keeps the first error, with the same text,
and returns false for every read after it, so each field is a
three-line read-and-assign and the decoder checks the error once.

Tried on unmarshalResponse first (it now takes the PDU type from its
caller, and its GetBulk/else branches hold two reads each), then used
for the community, the SNMPv1 trap header, the SNMPv3 header, the
scoped PDU and the USM parameters. The security model is still taken
only when something follows it; the version and the varbind name stay
explicit reads, as a check follows each before its value is used.
The table is consulted only for SNMPv3 traps; looking up a user for an
SNMPv1 or SNMPv2c trap would panic on its missing security parameters.
Passes on the code before the codec reads refactor too.
The field reader kept a pointer to its reader beside its error. Escape
analysis tracks a struct variable as one location, so returning the
error moved every reader it wrapped to the heap: two more allocations
per decoded message, three for SNMPv3. The reader now holds only the
error and each read takes the reader; readField is its one read, and
readInt, readString and readUint are gone. The version and the varbind
name, single reads each followed by its own check (the name once per
varbind), call readField directly.
A header field of another type than expected is skipped. For the USM
fields, whose parameters start as a copy of the decoder's, the
decoder's engine ID, boots, time and user name stay. Passes on the code
before the codec reads refactor too.
@ilyam8
ilyam8 added this pull request to stack #53 October 13, 2026 09:19
@ilyam8 ilyam8 changed the title codec reads refactor: untangle the codec's read side Oct 13, 2026
@ilyam8
ilyam8 marked this pull request as ready for review October 13, 2026 09:19
@ilyam8
ilyam8 merged commit 843aee0 into master Oct 13, 2026
20 checks passed
@ilyam8
ilyam8 deleted the codec-reads branch October 13, 2026 14:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants