Skip to content

corekit: reject short dynamic field buffers - #571

Open
lux-liang wants to merge 1 commit into
JarryShaw:mainfrom
lux-liang:fix/field-unpack-length-554
Open

lux-liang wants to merge 1 commit into
JarryShaw:mainfrom
lux-liang:fix/field-unpack-length-554

Conversation

@lux-liang

Copy link
Copy Markdown

Fixes #554.

Summary

  • reject short buffers when a field's length is resolved from packet context
  • preserve fixed-width short-read padding used by higher-level parser diagnostics
  • cover both byte buffers and streams, plus the existing compatibility behavior

Why

FieldBase.unpack() padded every short read to the declared field length. For a
callable length derived from packet data, that could create bytes that never
arrived. The parser now raises the existing FieldValueError before padding a
short dynamically sized field.

Verification

  • regression-first check on the original code: 2 failed, 1 passed
  • focused tests: 4 passed
  • corekit and schema tests: 128 passed, 125 subtests passed
  • unit tier: 1014 passed, 114 skipped, 2588 subtests passed
  • python -m compileall on both changed files
  • git diff --check

Stop callable field lengths from padding beyond the bytes that were read. Preserve the existing fixed-width padding behavior used by higher-level parser diagnostics.
@JarryShaw

Copy link
Copy Markdown
Owner

Thanks for this, and for spotting that a naive len(buffer) < length guard would break the library — you're right that fixed-width short reads have to keep padding, and building the discriminator around that is the correct instinct. Unfortunately the specific discriminator doesn't hold, and I don't think it can be made to.

The blocker: a truncated option stops parsing

Discriminating on whether the length came from packet context doesn't separate the attack from the legitimate short read, because both are wire-derived. A capture cut short by the snapshot length declares an over-long option length in exactly the same way a hostile one does.

OptionField.unpack rewinds and calls schema.unpack(...) at collections.py:420, which unpacks the option's data field — and that field is callable-length: BytesField(length=lambda pkt: pkt['length'] - 2) at schema/transport/tcp.py:321 and schema/internet/ipv4.py:259. So the guard fires on it.

A TCP segment with data offset 7, unassigned option kind 0x4f declaring length 12 with 6 data octets present — on main:

PARSED ok; options= [(<Option.Reserved_79: 79>, 12)]
SchemaWarning: packet length < 0: -4

That warning is the library noting the shortfall and carrying on, which is what #431 established. On this branch the same segment raises FieldValueError: Field data requires 10 octets, but only 6 are available., escaping TCP(...) unwrapped. Same at declared length 32.

The full suite stays green either way, which is why this wasn't obvious — that path has no test at all. I've filed that gap as #572, independently of this PR.

What's right

Your exception choice is correct: FieldValueError is the house pick for length mismatches in corekit/fields/. And your message spelling {self.name} is better than what I had — plain is 8/8 across pcapkit/corekit/, including five lines away in the same file. I'm taking that.

Where this leaves it

I'm going with #569, which bounds on magnitude instead: it rejects only when a declared length exceeds both the buffer and 262,144 octets (libpcap's MAXIMUM_SNAPLEN, and this package's own snaplen default), so every short read stays under the ceiling and keeps padding. So I'm closing this one — but the reasoning in it was sound and the short-read constraint you identified is exactly the thing that makes this fix difficult.

Two smaller notes for next time: tests for a field.py change belong in tests/corekit/test_fields_field.py rather than test_fields_strings.py, and CHANGELOG.md is generated from docs/source/changelog/1.5.0.rst by util/changelog_md.py. Also, your run showing 114 skipped suggests the sample captures weren't generated — python examples/generators/make_samples.py first, then it's 17.

Please do send more.

JarryShaw added a commit that referenced this pull request Sep 21, 2026
…ing (#554)

length is frequently wire-derived -- resolved by a Field's
_length_callback against the packet under parse, or built by a schema
selector from a value it just read off the wire (e.g.
DecryptionSecretsBlock's secrets_data field) -- and so attacker- or
corruption-controlled. buffer[:length].rjust(length, b'\x00') zero-padded
straight up to that length regardless of how little data buffer actually
held, so a ~40-octet PCAP-NG Decryption Secrets Block with a bogus inner
length could force a multi-gigabyte allocation.

- Raise FieldValueError when a declared length exceeds both the buffer's
  actual size and a 262144-octet (0x40_000) ceiling -- libpcap's own
  MAXIMUM_SNAPLEN, and this package's own default snaplen -- before the
  rjust() that would otherwise allocate and zero-fill on the packet's
  own say-so. FieldValueError, not BoolError ("must be a bool"), matches
  the precedent for an invalid/insufficient length in ListField.unpack.
- The ceiling is deliberate rather than "any shortfall is bogus":
  ListField.unpack and OptionField.unpack (collections.py) depend on a
  short, sometimes empty, tail read past a truncated option area
  decoding as zero, so an over-long ihl or a capture cut short by its
  snapshot length reads as end-of-option-list/Pad1 instead of raising
  (#431). An unconditional reject broke 15 existing tests exercising
  that mechanism; every one of those fields is a handful of octets,
  far under the ceiling, and is untouched.
- Add tests/corekit/test_fields_field.py: small-field short reads still
  zero-pad, the ceiling boundary in both directions, a full buffer past
  the ceiling is accepted, and a 16 GiB declared length against a
  2-octet buffer is rejected under a 5s deadline without allocating.
  Verified against three mutants (loosened comparison, shifted ceiling
  operator, wrong constant) to confirm the bound is exact.
- Update docs/source/changelog/1.5.0.rst and regenerate CHANGELOG.md.

Build: pytest tests -- 1271 passed, 17 skipped, 2850 subtests passed, 0
failed.

Spell the error message's field name plain rather than quoted, matching the 8/8 precedent across `pcapkit/corekit/` including `field.py:207` five lines away. Adopted from @lux-liang's #571, which proposed the same correction.
JarryShaw added a commit that referenced this pull request Sep 21, 2026
…ing (#554) (#569)

length is frequently wire-derived -- resolved by a Field's
_length_callback against the packet under parse, or built by a schema
selector from a value it just read off the wire (e.g.
DecryptionSecretsBlock's secrets_data field) -- and so attacker- or
corruption-controlled. buffer[:length].rjust(length, b'\x00') zero-padded
straight up to that length regardless of how little data buffer actually
held, so a ~40-octet PCAP-NG Decryption Secrets Block with a bogus inner
length could force a multi-gigabyte allocation.

- Raise FieldValueError when a declared length exceeds both the buffer's
  actual size and a 262144-octet (0x40_000) ceiling -- libpcap's own
  MAXIMUM_SNAPLEN, and this package's own default snaplen -- before the
  rjust() that would otherwise allocate and zero-fill on the packet's
  own say-so. FieldValueError, not BoolError ("must be a bool"), matches
  the precedent for an invalid/insufficient length in ListField.unpack.
- The ceiling is deliberate rather than "any shortfall is bogus":
  ListField.unpack and OptionField.unpack (collections.py) depend on a
  short, sometimes empty, tail read past a truncated option area
  decoding as zero, so an over-long ihl or a capture cut short by its
  snapshot length reads as end-of-option-list/Pad1 instead of raising
  (#431). An unconditional reject broke 15 existing tests exercising
  that mechanism; every one of those fields is a handful of octets,
  far under the ceiling, and is untouched.
- Add tests/corekit/test_fields_field.py: small-field short reads still
  zero-pad, the ceiling boundary in both directions, a full buffer past
  the ceiling is accepted, and a 16 GiB declared length against a
  2-octet buffer is rejected under a 5s deadline without allocating.
  Verified against three mutants (loosened comparison, shifted ceiling
  operator, wrong constant) to confirm the bound is exact.
- Update docs/source/changelog/1.5.0.rst and regenerate CHANGELOG.md.

Build: pytest tests -- 1271 passed, 17 skipped, 2850 subtests passed, 0
failed.

Spell the error message's field name plain rather than quoted, matching the 8/8 precedent across `pcapkit/corekit/` including `field.py:207` five lines away. Adopted from @lux-liang's #571, which proposed the same correction.
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.

Unbounded allocation in FieldBase.unpack: a wire-declared length drives rjust() with no bound

2 participants