Skip to content

fields: fix i2len of 3-byte fields returning 4 instead of 3#5048

Open
chuenchen309 wants to merge 1 commit into
secdev:masterfrom
chuenchen309:fix/threebytes-i2len
Open

fields: fix i2len of 3-byte fields returning 4 instead of 3#5048
chuenchen309 wants to merge 1 commit into
secdev:masterfrom
chuenchen309:fix/threebytes-i2len

Conversation

@chuenchen309

@chuenchen309 chuenchen309 commented Jul 19, 2026

Copy link
Copy Markdown
Contributor

Problem

The 3-byte field types (ThreeBytesField, X3BytesField, LEThreeBytesField, XLE3BytesField, OUIField) report a length of 4 via i2len(), even though they serialize and consume exactly 3 bytes. So a FieldLenField(length_of=<a 3-byte field>) auto-computes a length one byte too large:

from scapy.packet import Packet
from scapy.fields import FieldLenField, OUIField

class T(Packet):
    fields_desc = [FieldLenField("len", None, length_of="oui", fmt="B"),
                   OUIField("oui", 0x00000c)]

bytes(T())      # b'\x04\x00\x00\x0c'  -> len byte says 4, but OUI is 3 bytes on the wire
T(bytes(T())).len   # 4  (expected 3)

Cause

ThreeBytesField/LEThreeBytesField pass a 4-byte struct format ("!I"/"<I") to reuse struct for packing, then slice out 3 bytes in addfield/getfield. But self.sz = struct.calcsize(fmt) stays 4, and the inherited Field.i2len returns self.sz — so i2len (4) disagrees with the bytes actually emitted (3), and randval() would also draw from a 32-bit range for a 24-bit field. The sibling NBytesField builds an exact-width format and is self-consistent.

Fix

Set self.sz = 3 in the two base classes so i2len (and randval) match the wire size. addfield/getfield use hardcoded 3-byte slices, so nothing else reads sz and build/dissect round-trip is unchanged (verified for all five classes).

In-tree consumer (corrected)

There is one in-tree consumer, which my first push missed and CI caught: ISIS_TEDefaultMetricSubTlv wraps the 3-byte temetric field in a FieldLenField and, to compensate for the wrong i2len, subtracted 1 in an adjust= lambda to land back on the correct length of 3:

FieldLenField("len", None, length_of="temetric", adjust=lambda pkt, x: x - 1, fmt="B")

With i2len now correct, that -1 makes the sub-TLV length come out as 2. This PR removes the workaround so the length stays 3. test/contrib/isis.uts (LSP with Sub-TLVs) exercises it end to end and passes again.

Backward-compat note for out-of-tree layers: any user layer that similarly compensated for the old i2len (an adjust= that subtracted 1 on a 3-byte field) will now be off by one and needs the same one-line cleanup. If you'd rather preserve the old i2len == 4 behavior, say so and I'll close this.

Testing

  • test/fields.uts — added i2len == emitted == 3 assertions for all five classes plus a FieldLenField+OUIField TLV; red before, green after. PASSED=139 FAILED=0.
  • test/contrib/isis.utsPASSED=5 FAILED=0 (the LSP with Sub-TLVs roundtrip, which failed on the first push, now passes).
  • Verified in an isolated worktree with scapy.fields importing from the checkout.

Disclosure: found, written and verified by an AI coding agent (Claude Code) on this account (per the AI-Assisted commit trailer) — it wrote the repro, the fix and this description from running the code. I review every change and am accountable for it. Happy to adjust anything.

@codecov

codecov Bot commented Jul 19, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 80.22%. Comparing base (73435c1) to head (c8a4968).

Additional details and impacted files
@@           Coverage Diff           @@
##           master    #5048   +/-   ##
=======================================
  Coverage   80.22%   80.22%           
=======================================
  Files         388      388           
  Lines       96467    96469    +2     
=======================================
+ Hits        77394    77397    +3     
+ Misses      19073    19072    -1     
Files with missing lines Coverage Δ
scapy/contrib/isis.py 88.40% <ø> (ø)
scapy/fields.py 92.79% <100.00%> (+<0.01%) ⬆️

... and 5 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

ThreeBytesField and LEThreeBytesField pass a 4-byte struct format
("!I"/"<I") to reuse struct for packing, then slice out 3 bytes in
addfield/getfield. But self.sz stayed 4 (struct.calcsize), so the
inherited Field.i2len returns 4 for a field that emits 3 bytes, and
randval() would draw from a 32-bit range for a 24-bit field. A
FieldLenField(length_of=<a 3-byte field>) therefore auto-computes a
length one byte too large.

Set self.sz = 3 so i2len (and randval) match the bytes actually
emitted. addfield/getfield use hardcoded 3-byte slices, so nothing else
reads sz and round-trip is unaffected.

There is one in-tree consumer: ISIS_TEDefaultMetricSubTlv wraps the
3-byte temetric field in a FieldLenField and, to work around the wrong
i2len, subtracted 1 in an adjust= lambda to land back on the correct
length of 3. With i2len now correct, drop that -1 so the sub-TLV length
stays 3 (test/contrib/isis.uts "LSP with Sub-TLVs" covers it end to end).

Note for out-of-tree layers: any code that similarly compensated for the
old i2len (e.g. an adjust= that subtracted 1) will now be off by one and
needs the same one-line cleanup.

AI-Assisted: yes (Claude Opus 4.8)
Co-authored-by AI: Claude (Claude Code).

Signed-off-by: Andrew Chen <48723787+chuenchen309@users.noreply.github.com>
@chuenchen309
chuenchen309 force-pushed the fix/threebytes-i2len branch from d2abafd to c8a4968 Compare July 19, 2026 06:12
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.

1 participant