Getting a decoder into Chrome was one milestone. Hardening it and enabling it by default completed the next stage.
Status: โ Enabled by Default for Chrome 155
After the Initial Integration
The original JPEG XL integration added a memory-safe Rust decoder. Since then the work has covered ordinary browser concerns: partial input, paint requests arriving before headers, threading, MIME sniffing, and fuzzers.
That is a good sign. A codec starts feeling native when its bugs are the same unglamorous bugs every mature decoder has to solve.
Intent to Ship and Chrome 155
We filed the Intent to Ship, and the Blink API Owners gate received the required three LGTMs.
ChromeStatus targets JPEG XL decoding for Chrome 155 on desktop, Android, and WebView. The default-enable change has landed for that release:
It enables both decoding and image/jxl advertising in the Accept header by default. kJXLImageFormat remains as a kill switch, and the virtual test suite is inverted to exercise the disabled configuration after the default changes. The CL also keeps incremental animation decoding sequential until the pixel decoder is initialized, avoiding an invalid early seek.
The I2S review covers the decoder's security model and test coverage: Rust memory safety, renderer sandboxing, Chromium and upstream fuzzing, controlled allocation failures, conformance tests, and deterministic testing of parallel schedules.
Making Fuzzing Part of the Upstream Loop
Fuzzing needed to cover both Chromium and the upstream crate. Fixing every finding only in the browser would leave other jxl-rs embedders without the same corrections.
ClusterFuzzLite was added to jxl-rs with short AddressSanitizer runs on pull requests and longer scheduled runs against the existing decode targets. It soon produced several arithmetic and allocation findings.
The first wave found arithmetic and allocation assumptions hidden behind valid Rust types:
- MERGED Prevent extra-channel name length underflow
- MERGED Prevent overflow in SmallBuffer::refill
- MERGED Use checked arithmetic for dimensions, patches, splines, and render groups
- MERGED Make section-buffer allocation fallible
- MERGED Validate blending alpha-channel indices
- MERGED Bound frame-index allocation by remaining input
Rust prevents these from becoming ordinary out-of-bounds writes or use-after-free bugs. It does not automatically make a panic or an infallible multi-gigabyte allocation acceptable in a renderer. A browser decoder still has to turn arbitrary bytes into either pixels or a controlled error.
From Crashes to Invariants
Later findings were less about one arithmetic operation and more about decoder invariants:
- MERGED Use clipped blend width for missing references
- MERGED Use fallible allocation for HF sections
- MERGED Validate histogram indices
- MERGED Prevent one-pixel save-region underflow
- MERGED Grow section buffers from available input
The last issue came from a 9-byte truncated codestream whose TOC claimed a 743 MB section. Section buffers now grow from bytes actually available, not declarations made by untrusted input.
That last issue came from Chromium's blink_jxl_decoder_fuzzer, while the reproducer and generic fix live in jxl-rs. The next crate roll then brings the upstream correction back into Chromium.
Fuzzing Thread Schedules Too
Once multi-threaded decoding entered the picture, bytes were no longer the only input worth fuzzing. Shuttle explores thread schedules deterministically, turning rare races into replayable tests.
That work found reentrant locks, incorrect read/write lock ownership, schedule-dependent border rows, and progressive output that depended on group completion order:
- MERGED Avoid an aliased squeeze-buffer reentrant lock
- MERGED Use floor semantics for vertically subsampled border rows
- MERGED Take read locks for palette prediction context
- MERGED Make scheduling sets deterministic so failures replay
- MERGED Fix smooth-unsqueeze scratch-row races
- MERGED Clip ready rectangles before pipeline stages consume them
This is the less visible half of enabling a parallel runner. The happy path getting faster is useful; proving that output does not change with a different interleaving is what makes it shippable.
The 4096-Byte Stall
Progressive images should render as data arrives. A page should not need the full file before showing the first useful pixels.
The jxl-rs codestream parser eagerly reads input into a 4096-byte internal buffer while parsing headers. Chromium's decode loop saw that all external input had been consumed and stopped calling process(). But jxl-rs still had buffered bytes to drain. The result: no partial image until the network delivered more than the first 4096 bytes.
The fix is subtle but simple: external input being empty does not mean the decoder has no work. Keep calling process() while the internal parser can make progress.
A related bug happened at the other end of initialization: Chromium tried to flush partial pixels before the decoder had selected a pixel format. jxl-rs correctly aborted at an internal unwrap(). The browser now waits until basic image information exists before flushing.
Paint Can Arrive Before the Header
Browser image decoders do not control every call order. Paint code can ask whether an image repeats before enough bytes have arrived to parse its basic header.
The JXL path used to CHECK that basic_info_ existed. A valid streaming sequence could therefore crash the renderer before the header was complete. Returning kAnimationNone until the information exists is both safe and consistent with an image whose animation state is not known yet.
More Cores, Same Decoder
Large images should not decode on one core when the Rust decoder already supports parallel work. Chromium now wires jxl-rs's parallel runner into the decode path rather than implementing a second threading model around it.
The decoder itself also moved forward through the normal Rust roll from 0.4.3 to 0.5.1.
- MERGED Roll jxl-rs 0.4.3 to 0.5.1
The Less Visible Chromium Work
MIME sniffing now handles complete image data shorter than the longest known signature, stale web-test expectations are gone, and the feature flag remains available as a kill switch after default enablement. The Chromium AVIF and JXL fuzzers also dropped an invalid timeout_per_input option so ClusterFuzz runs the configuration it actually understands.
None of that makes a good launch screenshot. It is exactly what makes a decoder trustworthy.
Links
- ChromeStatus: JPEG XL decoding support
- Intent to Ship thread
- Flag flip: enable JPEG XL by default
- Original JPEG XL integration post
- ClusterFuzzLite integration in jxl-rs
- ClusterFuzz: fallible section allocation
- Chromium fuzzer: do not trust TOC-declared allocation size
- Shuttle concurrency fixes
- 4096-byte progressive stall
- Early partial-pixel flush
- RepetitionCount crash
- Multi-threaded decode