Fanbase

jxl/jxl

version 1.0

Raw JPEG XL codestreams with a real bit-packed header and metadata; the entropy-coded frame body is a structurally-framed stub, so files do not decode

Format
JPEG XL (jxl)
Authors
The Fandango Fuzzer Team
License
Apache-2.0
Fandango
>=1.3
Extensions
jxl
Media type
image/jxl
Specification
https://www.iso.org/standard/85066.html
Its files should decode
never
In the registry
specs/jxl/jxl/jxl.fan

Use it

fanbase install jxl
fandango fuzz -F jxl -n 10

How it does

accepts 0%, covers 15.7%, 19/s. 1000 inputs, seed 1.

ParserAcceptsVersion
djxl0%djxl v0.11.2 [AVX2,SSE4,SSE2]
libjxl-cov0%libjxl 0.12.0

The spec says its files decode never: as expected.

libjxl-cov

lines reached
15.7% of 16349
real files reach
50.5%
only the generated files
230 lines
only the real files
5908 lines
8.4% after 1 inputs, 15.7% after 1000real files: 50.5%

Cite it

@software{fanbase_jxl_jxl_1_0,
  author  = {{The Fandango Fuzzer Team}},
  title   = {JPEG XL (jxl/jxl), a Fandango input specification},
  version = {1.0},
  url     = {https://github.com/fandango-fuzzer/fanbase},
  note    = {Fanbase registry, specs/jxl/jxl/jxl.fan, sha256 1ef36a8cf070},
}

Source

Source (364 lines)
# JPEG XL raw codestream (ISO/IEC 18181-1): signature, SizeHeader, ImageMetadata
# (bit depth, extra channels, colour encoding, tone mapping, ICC), an optional
# preview, and one frame header + TOC. Header fields are bit-packed to match
# libjxl's field order exactly; the entropy-coded frame body is a
# structurally-framed stub, so files rarely decode.
#
# TODO:
# * The frame body is a stub (no ANS/MA-tree entropy coding), so files won't
#   round-trip through a real decoder. A real fix needs a stateful encoder,
#   not a CFG.
# * Relies on Generators alot, modify in the future

<start> ::= <jxl>

# Flavor alternation — each branch is a distinct nonterminal so the alternation
# fuzzes cleanly (no shared constrained sibling), and each maps to one call of
# the bit-packing sealer. Together they exercise every major header element.
<jxl> ::= <f_tiny_gray8>   \
        | <f_small_rgb8>    \
        | <f_med_rgb8>      \
        | <f_large_rgb8>    \
        | <f_ratio_wide>    \
        | <f_gray16>        \
        | <f_rgb16>         \
        | <f_rgb_float32>   \
        | <f_rgba8>         \
        | <f_xyb>           \
        | <f_oriented>      \
        | <f_preview>       \
        | <f_animation>     \
        | <f_modular>       \
        | <f_icc>

<f_tiny_gray8>  ::= <blob> := build_jxl(0)
<f_small_rgb8>  ::= <blob> := build_jxl(1)
<f_med_rgb8>    ::= <blob> := build_jxl(2)
<f_large_rgb8>  ::= <blob> := build_jxl(3)
<f_ratio_wide>  ::= <blob> := build_jxl(4)
<f_gray16>      ::= <blob> := build_jxl(5)
<f_rgb16>       ::= <blob> := build_jxl(6)
<f_rgb_float32> ::= <blob> := build_jxl(7)
<f_rgba8>       ::= <blob> := build_jxl(8)
<f_xyb>         ::= <blob> := build_jxl(9)
<f_oriented>    ::= <blob> := build_jxl(10)
<f_preview>     ::= <blob> := build_jxl(11)
<f_animation>   ::= <blob> := build_jxl(12)
<f_modular>     ::= <blob> := build_jxl(13)
<f_icc>         ::= <blob> := build_jxl(14)

# Generic placeholder; the := generator on each flavor overwrites it wholesale
# (the produced bytes re-parse trivially as a run of <byte>).
<blob> ::= <byte>+


# Preamble: the JXL bit-writer and the header packer.
import os


class BitWriter:
    # LSB-first bit accumulator matching libjxl's BitReader consumption order.
    def __init__(self):
        self.acc = 0
        self.nbits = 0

    def write(self, value, count):
        # Append `count` low bits of `value`, least-significant bit first.
        value &= (1 << count) - 1
        self.acc |= value << self.nbits
        self.nbits += count

    def zero_pad_to_byte(self):
        rem = self.nbits % 8
        if rem:
            self.nbits += 8 - rem

    def to_bytes(self):
        out = bytearray()
        acc = self.acc
        n = self.nbits
        pos = 0
        while pos < n:
            out.append((acc >> pos) & 0xFF)
            pos += 8
        return bytes(out)


def write_u32(bw, encs, value):
    # U32 encoding: a 2-bit selector chooses one of four sub-encodings, each
    # either a literal value ('val', k) or an offset + fixed bit width
    # ('bits', nbits, offset) meaning value = offset + read(nbits).
    for idx, enc in enumerate(encs):
        if enc[0] == 'val':
            if value == enc[1]:
                bw.write(idx, 2)
                return
        else:
            nbits = enc[1]
            offset = enc[2]
            if offset <= value < offset + (1 << nbits):
                bw.write(idx, 2)
                bw.write(value - offset, nbits)
                return
    raise ValueError('value %d not representable by U32 encs %r' % (value, encs))


def write_u64(bw, value):
    # U64: selector u(2). 0 -> 0; 1 -> u(4); 2 -> 12 + u(8);
    # 3 -> 20 bits then, while a continuation bit is set, 8 more bits.
    if value == 0:
        bw.write(0, 2)
        return
    if 1 <= value <= 15:
        bw.write(1, 2)
        bw.write(value, 4)
        return
    if 16 <= value <= 12 + 255:
        bw.write(2, 2)
        bw.write(value - 12, 8)
        return
    bw.write(3, 2)
    bw.write(value & 0xFFFFF, 20)
    value >>= 20
    while value:
        bw.write(1, 1)
        bw.write(value & 0xFF, 8)
        value >>= 8
    bw.write(0, 1)


# U32 distribution used for both ysize and xsize in SizeHeader (offset 1).
DIM_ENC = [('bits', 9, 1), ('bits', 13, 1), ('bits', 18, 1), ('bits', 30, 1)]

# Aspect-ratio table for SizeHeader.ratio 1..7 (num, den): xsize = ysize*num/den.
RATIO_TABLE = {1: (1, 1), 2: (12, 10), 3: (4, 3), 4: (3, 2),
               5: (16, 9), 6: (5, 4), 7: (2, 1)}


def write_size_header(bw, ysize, xsize, ratio):
    # small_ is true when both dims are multiples of 8 in [8,256].
    small = (ysize % 8 == 0 and 8 <= ysize <= 256 and
             xsize % 8 == 0 and 8 <= xsize <= 256)
    bw.write(1 if small else 0, 1)
    if small:
        bw.write(ysize // 8 - 1, 5)
    else:
        write_u32(bw, DIM_ENC, ysize)
    bw.write(ratio, 3)
    if ratio == 0:
        if small:
            bw.write(xsize // 8 - 1, 5)
        else:
            write_u32(bw, DIM_ENC, xsize)
    # ratio != 0 -> xsize is derived by the decoder from ysize and the table.


def write_bit_depth(bw, floating, bits_per_sample, exp_bits):
    bw.write(1 if floating else 0, 1)
    if floating:
        # bits_per_sample: U32(Val(32), Val(16), Val(24), Bits(6)+1)
        write_u32(bw, [('val', 32), ('val', 16), ('val', 24), ('bits', 6, 1)],
                  bits_per_sample)
        bw.write(exp_bits - 1, 4)
    else:
        # bits_per_sample: U32(Val(8), Val(10), Val(12), Bits(6)+1)
        write_u32(bw, [('val', 8), ('val', 10), ('val', 12), ('bits', 6, 1)],
                  bits_per_sample)


def write_customxy(bw, x, y):
    # CustomXY: two U32 signed-folded coords (unused unless custom primaries).
    write_u32(bw, [('bits', 19, 0), ('bits', 19, 524288),
                   ('bits', 20, 1048576), ('bits', 21, 2097152)], x & 0x1FFFFF)
    write_u32(bw, [('bits', 19, 0), ('bits', 19, 524288),
                   ('bits', 20, 1048576), ('bits', 21, 2097152)], y & 0x1FFFFF)


def write_color_encoding(bw, gray, want_icc, color_space, white_point,
                         primaries, transfer, rendering_intent):
    bw.write(0, 1)                 # all_default = false (fields set explicitly)
    bw.write(1 if want_icc else 0, 1)
    bw.write(color_space, 2)       # 0 RGB, 1 Gray, 2 XYB, 3 Unknown
    if want_icc:
        return
    # white_point: Enum u(2)-ish (0 D65, 1 Custom, 2 E, 3 DCI)
    bw.write(white_point, 2)
    if white_point == 1:
        write_customxy(bw, 0, 0)
    if color_space != 2:           # not XYB
        bw.write(primaries, 2)     # 0 sRGB, 1 Custom, 2 2100, 3 P3
        if primaries == 1:
            write_customxy(bw, 0, 0)
            write_customxy(bw, 0, 0)
            write_customxy(bw, 0, 0)
    # transfer function: have_gamma bool; if not, Enum tf
    bw.write(0, 1)                 # have_gamma = false
    bw.write(transfer, 5)          # transfer-function enum (u(5) domain here)
    bw.write(rendering_intent, 2)  # 0 perceptual,1 relative,2 saturation,3 abs


def write_extra_channel(bw, ec_type, bits_per_sample):
    bw.write(0, 1)                 # all_default = false
    bw.write(ec_type, 4)           # 0 Alpha,1 Depth,2 SpotColor,3 SelectionMask,...
    write_bit_depth(bw, False, bits_per_sample, 8)
    write_u32(bw, [('val', 0), ('val', 3), ('val', 4), ('bits', 3, 1)], 0)  # dim_shift
    # name length = 0
    write_u32(bw, [('val', 0), ('bits', 4, 0), ('bits', 16, 0), ('bits', 32, 0)], 0)
    if ec_type == 0:
        bw.write(0, 1)             # alpha_associated = false
    if ec_type == 2:
        write_u32(bw, [('bits', 8, 0)] * 4, 0)  # spot color placeholder


def write_preview_header(bw):
    bw.write(0, 1)                 # div8 = false
    write_u32(bw, [('bits', 5, 1), ('bits', 9, 1),
                   ('bits', 10, 1), ('bits', 12, 1)], 32)   # ysize
    bw.write(0, 3)                 # ratio = 0
    write_u32(bw, [('bits', 5, 1), ('bits', 9, 1),
                   ('bits', 10, 1), ('bits', 12, 1)], 32)   # xsize


def write_animation_header(bw):
    write_u32(bw, [('val', 100), ('val', 1000), ('bits', 10, 1),
                   ('bits', 30, 1)], 100)                   # tps_numerator
    write_u32(bw, [('val', 1), ('val', 1001), ('bits', 8, 1),
                   ('bits', 10, 1)], 1)                     # tps_denominator
    write_u32(bw, [('val', 0), ('bits', 3, 0), ('bits', 16, 0),
                   ('bits', 32, 0)], 0)                     # num_loops = infinite
    bw.write(0, 1)                                          # have_timecodes = false


def write_image_metadata(bw, spec):
    # spec keys: extra_fields, orientation, have_preview, have_animation,
    # floating, bps, exp_bits, num_extra, ec_type, xyb, gray, want_icc,
    # color_space, white_point, primaries, transfer, rendering_intent.
    if spec is None:
        bw.write(1, 1)             # all_default = true (8-bit sRGB RGB, XYB)
        return
    bw.write(0, 1)                 # all_default = false
    bw.write(1 if spec['extra_fields'] else 0, 1)
    if spec['extra_fields']:
        bw.write(spec['orientation'] - 1, 3)
        bw.write(0, 1)             # have_intrinsic_size = false
        bw.write(1 if spec['have_preview'] else 0, 1)
        if spec['have_preview']:
            write_preview_header(bw)
        bw.write(1 if spec['have_animation'] else 0, 1)
        if spec['have_animation']:
            write_animation_header(bw)
    write_bit_depth(bw, spec['floating'], spec['bps'], spec['exp_bits'])
    bw.write(1, 1)                 # modular_16bit_buffers = true
    write_u32(bw, [('val', 0), ('val', 1), ('bits', 4, 2), ('bits', 12, 1)],
              spec['num_extra'])
    for _ in range(spec['num_extra']):
        write_extra_channel(bw, spec['ec_type'], 8)
    bw.write(1 if spec['xyb'] else 0, 1)   # xyb_encoded
    write_color_encoding(bw, spec['gray'], spec['want_icc'], spec['color_space'],
                         spec['white_point'], spec['primaries'],
                         spec['transfer'], spec['rendering_intent'])
    if spec['extra_fields']:
        bw.write(1, 1)             # tone_mapping all_default = true
    write_u64(bw, 0)               # extensions = none


def write_frame_stub(bw, frame_kind, duration):
    # FrameHeader (partial): not all_default, so the encoding varies; then
    # byte-align and append a framed but non-entropy-decodable group-data
    # stub. Real group data is ANS/prefix coded and out of CFG reach.
    bw.write(0, 1)                 # all_default = false
    bw.write(0, 2)                 # frame_type = kRegularFrame
    bw.write(frame_kind, 1)        # encoding: 0 VarDCT, 1 Modular
    write_u64(bw, 0)               # flags = 0
    if frame_kind == 0:
        bw.write(0, 1)             # do_YCbCr = false (VarDCT path)
    bw.write(0, 2)                 # upsampling shift = 0
    bw.write(0, 2)                 # passes: num_passes selector -> 1 pass
    bw.write(0, 2)                 # x0/y0 (not full frame flag path) placeholder
    bw.write(0, 1)                 # is_last -> false so a duration can apply
    bw.write(0, 2)                 # save_as_reference etc. placeholder
    if duration is not None:
        write_u32(bw, [('val', 0), ('val', 1), ('bits', 8, 0),
                       ('bits', 32, 0)], duration)
    bw.write(0, 1)                 # have_crc / name_len = 0 placeholder


def build_jxl(flavor):
    # Flavor -> (ysize, xsize, ratio, metadata-spec, frame_kind, duration).
    # metadata-spec None means the compact all_default ImageMetadata.
    def meta(**kw):
        base = dict(extra_fields=False, orientation=1, have_preview=False,
                    have_animation=False, floating=False, bps=8, exp_bits=8,
                    num_extra=0, ec_type=0, xyb=False, gray=False, want_icc=False,
                    color_space=0, white_point=0, primaries=0, transfer=8,
                    rendering_intent=0)
        base.update(kw)
        return base

    table = [
        # 0: tiny 8x8 grayscale, 8-bit
        (8, 8, 0, meta(gray=True, color_space=1), 0, None),
        # 1: small 64x64 RGB 8-bit, compact all_default metadata
        (64, 64, 0, None, 0, None),
        # 2: medium 256x192 RGB 8-bit
        (256, 192, 0, None, 0, None),
        # 3: large 4096x2048 RGB 8-bit (exercises the wide U32 dim encoding)
        (4096, 2048, 0, None, 0, None),
        # 4: aspect-ratio-derived width, ratio=5 (16:9), ysize 720
        (720, 1280, 5, None, 0, None),
        # 5: grayscale 16-bit
        (200, 200, 0, meta(gray=True, color_space=1, bps=16), 0, None),
        # 6: RGB 16-bit
        (300, 300, 0, meta(bps=16), 0, None),
        # 7: RGB float32
        (128, 128, 0, meta(floating=True, bps=32, exp_bits=8), 0, None),
        # 8: RGBA (one alpha extra channel), 8-bit
        (150, 150, 0, meta(num_extra=1, ec_type=0), 0, None),
        # 9: XYB-encoded RGB
        (100, 100, 0, meta(xyb=True, color_space=2), 0, None),
        # 10: oriented (rotate 90 CW = orientation 6), extra_fields
        (240, 160, 0, meta(extra_fields=True, orientation=6), 0, None),
        # 11: preview image present
        (400, 300, 0, meta(extra_fields=True, have_preview=True), 0, None),
        # 12: animation (multi-frame), duration on the first frame
        (96, 96, 0, meta(extra_fields=True, have_animation=True), 0, 30),
        # 13: Modular-encoded frame
        (128, 96, 0, meta(bps=8), 1, None),
        # 14: embedded-ICC colour encoding (want_icc)
        (180, 120, 0, meta(want_icc=True, color_space=0), 0, None),
    ]
    ysize, xsize, ratio, spec, frame_kind, duration = table[flavor % len(table)]

    bw = BitWriter()
    write_size_header(bw, ysize, xsize, ratio)
    write_image_metadata(bw, spec)

    out = bytearray()
    out += b'\xff\x0a'                       # 1. Signature
    out += bw.to_bytes()                     # 2/3. SizeHeader + ImageMetadata

    # 4. Optional embedded ICC blob (want_icc): a short byte-aligned stub.
    if spec is not None and spec.get('want_icc'):
        out += b'\x00' * 16

    # 6. Frame region: byte-aligned FrameHeader + framed group-data stub.
    fbw = BitWriter()
    write_frame_stub(fbw, frame_kind, duration)
    fbw.zero_pad_to_byte()
    out += fbw.to_bytes()

    # Structural (non-decodable) group-data stub; os.urandom gives per-file
    # uniqueness so a -n run yields distinct outputs.
    body_len = 64 + (flavor * 8)
    out += os.urandom(body_len)

    # 12 (animation): append one more framed stub so the codestream carries a
    # second frame, and mark it as the last.
    if spec is not None and spec.get('have_animation'):
        gbw = BitWriter()
        write_frame_stub(gbw, frame_kind, 20)
        gbw.zero_pad_to_byte()
        out += gbw.to_bytes()
        out += os.urandom(48)

    return bytes(out)