What
Found while fixing #477. pcapkit/toolkit/scapy.py:160:
```python
data = IP_Packet(
...
fo=ipv4.frag, # fragment offset
...
)
```
`ipv4.frag` is scapy's raw IPv4 fragment-offset field — a 13-bit value counted in 8-octet units, per the wire format — not a byte offset. Every other pcapkit toolkit adapter either multiplies by 8 explicitly or uses a value the underlying engine has already scaled:
- `pcapkit/toolkit/dpkt.py:215`: `fo=ipv4.offset * 8`
- `pcapkit/toolkit/pypcapfile.py:326`: `fo=ipv4.off * 8`
- `pcapkit/toolkit/pcap.py:67` / `pcapkit/toolkit/pcapng.py:70`: `fo=ipv4_info.offset` (pcapkit's own `ipv4.py` sets `offset=int(schema.flags['offset']) * 8` at `pcapkit/protocols/internet/ipv4.py:307`, i.e. already in octets)
- `pcapkit/toolkit/scapy.py:225` (IPv6 fragment header, same file): `fo=ipv6_frag.offset * 8` — scapy's IPv6 fragment path does scale it; only the IPv4 path at line 160 does not
Reproduction (not yet executed against the reassembler, confirmed only at the field level)
```python
import scapy.all as sc
p = sc.IP(frag=5, flags='MF')
bytes(p)[6:8].hex()
'2005'
```
`0x2005` decodes as flags=`001` (MF set) + a 13-bit offset of `5` — i.e. scapy stored the raw unit count `5`, not a byte offset of `40`. `pcapkit.toolkit.scapy.ipv4_reassembly` would hand this packet to `IP.reassembly()` as `fo=5`, when every other engine reading the same bytes would report `fo=40`.
Expected impact
Any capture read through the scapy engine (`pcapkit.foundation.engines.scapy`) containing IPv4 fragments would compute fragment placement 8x too small, corrupting reassembly for that engine specifically — silently, since nothing validates `fo` against the fragment's own length. IPv6 fragments read through the same engine are unaffected (the Fragment-header path already multiplies by 8).
Scope
Not fixing here — this is a single-line, single-adapter fix (`fo=ipv4.frag * 8`) but wants its own verification against an actual reassembly run through the scapy engine (e.g. a scapy-crafted fragmented IPv4 packet fed through `pcapkit.foundation.engines.scapy`) and its own test, rather than being folded into #477/#482, which own `pcapkit/foundation/reassembly/ip.py` and its data model, not `pcapkit/toolkit/scapy.py`.
What
Found while fixing #477.
pcapkit/toolkit/scapy.py:160:```python
data = IP_Packet(
...
fo=ipv4.frag, # fragment offset
...
)
```
`ipv4.frag` is scapy's raw IPv4 fragment-offset field — a 13-bit value counted in 8-octet units, per the wire format — not a byte offset. Every other pcapkit toolkit adapter either multiplies by 8 explicitly or uses a value the underlying engine has already scaled:
Reproduction (not yet executed against the reassembler, confirmed only at the field level)
```python
`0x2005` decodes as flags=`001` (MF set) + a 13-bit offset of `5` — i.e. scapy stored the raw unit count `5`, not a byte offset of `40`. `pcapkit.toolkit.scapy.ipv4_reassembly` would hand this packet to `IP.reassembly()` as `fo=5`, when every other engine reading the same bytes would report `fo=40`.
Expected impact
Any capture read through the scapy engine (`pcapkit.foundation.engines.scapy`) containing IPv4 fragments would compute fragment placement 8x too small, corrupting reassembly for that engine specifically — silently, since nothing validates `fo` against the fragment's own length. IPv6 fragments read through the same engine are unaffected (the Fragment-header path already multiplies by 8).
Scope
Not fixing here — this is a single-line, single-adapter fix (`fo=ipv4.frag * 8`) but wants its own verification against an actual reassembly run through the scapy engine (e.g. a scapy-crafted fragmented IPv4 packet fed through `pcapkit.foundation.engines.scapy`) and its own test, rather than being folded into #477/#482, which own `pcapkit/foundation/reassembly/ip.py` and its data model, not `pcapkit/toolkit/scapy.py`.