Skip to content

fix: IPv4._make_data mis-scales fragment offset and crashes without options - #499

Merged
JarryShaw merged 2 commits into
mainfrom
fix/494-ipv4-make-data-offset
Sep 19, 2026
Merged

JarryShaw merged 2 commits into
mainfrom
fix/494-ipv4-make-data-offset

Conversation

@JarryShaw

Copy link
Copy Markdown
Owner

Fixes #494. Also fixes the two ipv4.py docstring items from #490 — that file is the seam between the two issues, so they land together rather than conflicting.

Defect 1 — offset handed back unscaled, an 8x inflation

RFC 791 makes the wire Fragment Offset a count of 8-octet units. read() scales it up to octets for the data model, so _make_data() must scale it back down. IPv6_Frag does; IPv4 did not:

IPv4.read            (ipv4.py:307):      offset=int(schema.flags['offset']) * 8
IPv6_Frag.read       (ipv6_frag.py:147): offset=int(schema.flags['offset']) * 8
IPv6_Frag._make_data (ipv6_frag.py:266): 'offset': data.offset // 8,     <- correct
IPv4._make_data      (ipv4.py:529):      'offset': data.offset,          <- missing // 8

IPv6_Frag._make_data carries an explicit NOTE stating why the division is needed, which is the strongest evidence this was an omission rather than a decision. The fix mirrors it, comment included.

Defect 2 — .options read unconditionally

read() (ipv4.py:315-318) only sets options when hdr_len > 20, but _make_data read data.options unconditionally, so the ordinary no-options packet raised AttributeError: 'IPv4' object has no attribute 'options'.

Now getattr(data, 'options', None), matching the identical pattern already used for the same optional-field problem at pcapkit/protocols/transport/tcp.py:664. None is not an invented sentinel — it is make()'s own declared default for options, confirmed with inspect.signature.

The ordering, which the original report had wrong

Defect 2 fires first. For a packet with no options the AttributeError raises before _make_data can return the unconverted offset, so defect 1 is unreachable by that route; it bites only when the packet does carry options and hdr_len > 20. Both are real, and whichever were fixed alone would expose the other — hence one change.

Defect 3 — two docstrings, from #490

Both confirmed against inspect.signature before editing:

  • _make_opt_sec (:1350) documented a phantom sec and left all five real parameters undocumented — now level, level_default, level_namespace, level_reversed, authorities.
  • _make_opt_qs (:1764) documented code/opt; the real names are kind/option. The origin is a copy from hopopt.py:1656/ipv6_opts.py:1668, which define their own _make_opt_qs with exactly that naming. Every other _make_opt_* in this file already used kind/option.

Verification

Measured on this branch with pcapkit.__file__ asserted against the worktree before import:

wire offset field (8-octet units): 5
data.offset after read() (octets):  40
hasattr(data, 'options'):           False
_make_data kwargs['offset']:        5      (was 40)
_make_data kwargs['options']:       None   (was AttributeError)
re-packed wire offset field:        5
byte-identical round trip:          True

And with options present, the case where defect 1 was reachable at all: hdr_len 28, data.offset 40, _make_data now returns 5.

Revert-proof. With both fixes reverted in place, the new test fails; restored, it passes:

FAILED tests/protocols/internet/test_ipv4_unit.py::IPv4UnitTests::test_ipv4_make_data_scales_offset_and_defaults_missing_options
1 failed, 12 deselected

Tests: test_ipv4_unit.py 13 passed, 16 subtests. Blast radius test_ipv6_extension_unit.py + test_option_roundtrip_unit.py 58 passed, 436 subtests.

Why the new test looks the way it does

The existing test_ipv4_make_data_preserves_selected_fields uses a DummyDict fixture with offset=0 and always supplies options, and never asserts values['offset']. Zero is the one value for which the missing // 8 is invisible, and supplying options hides the other defect — so neither could ever have been caught. The new test uses a real make()/read() round trip with wire offset 5 and no options, which are exactly the two conditions the old fixture avoided.

Found while verifying, deliberately not fixed here

IPv4.from_data() on a parsed packet is broken independently of this change, and remains broken after it — the failure mode merely shifts. On main it raises FieldValueError: Field options has invalid value; on this branch, ProtocolUnbound: unsupported type <class 'pcapkit.protocols.misc.raw.Raw'>. Both predate this PR in substance: _make_data is only one input to from_data, and the remaining fault is in how _make_payload/_make_ipv4_options handle a parsed Raw payload and a parsed options container. That is a separate defect and belongs in its own issue rather than being folded in here.

…ptions

Fixes #494; two docstring fixes from #490 in the same file.

- IPv4._make_data returned data.offset (octets) straight to make(), which
  expects the on-wire 8-octet-unit value; scale back down with `// 8`,
  mirroring the existing, correctly-commented IPv6_Frag._make_data.
- IPv4._make_data read data.options unconditionally, even though read()
  only sets it when hdr_len > 20; use getattr(data, 'options', None) so
  the ordinary no-options case no longer raises AttributeError. This
  defect fired first, which is why the offset bug was unreachable via a
  no-options packet.
- IPv4._make_opt_sec docstring documented a nonexistent `sec` parameter
  and omitted the five real ones (level, level_default, level_namespace,
  level_reversed, authorities).
- IPv4._make_opt_qs docstring documented `code`/`opt`, copied from the
  unrelated hopopt.py/ipv6_opts.py `_make_opt_qs`; real params are
  `kind`/`option`.
- Added a regression test using a non-zero offset and a real packet with
  no options -- the two conditions the existing DummyDict fixture (offset
  0, options always present) could not exercise.

Verified with tests/protocols/internet/test_ipv4_unit.py (13 passed),
test_ipv6_extension_unit.py and test_option_roundtrip_unit.py (58 passed,
436 subtests) as blast-radius checks.
@JarryShaw
JarryShaw merged commit 4eac982 into main Sep 19, 2026
23 checks passed
@JarryShaw
JarryShaw deleted the fix/494-ipv4-make-data-offset branch September 19, 2026 03:17
@JarryShaw JarryShaw added fix Pull requests that fix a defect (fix: subject prefix) breaking Breaks public-facing behaviour or API (apply alongside the type label) labels Sep 22, 2026
@JarryShaw JarryShaw added this to the 1.5 milestone Oct 6, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

breaking Breaks public-facing behaviour or API (apply alongside the type label) fix Pull requests that fix a defect (fix: subject prefix)

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

IPv4._make_data: fragment offset returned unscaled (8x), and .options read unconditionally

1 participant