Skip to content

Commit 1b40269

Browse files
committed
added firefox blog
1 parent ab46df7 commit 1b40269

4 files changed

Lines changed: 129 additions & 0 deletions

File tree

content/_index.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ supporters = [
3434
]
3535

3636
blogposts = [
37+
"zlib-rs in Firefox",
3738
"Announcing Zstandard in Rust",
3839
"Three years of Rusty sudo",
3940
"Announcing ntpd-rs in Ubuntu",

content/blog/zlib-rs in Firefox.md

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
+++
2+
title = "zlib-rs in Firefox"
3+
slug = "zlib-rs-in-firefox"
4+
authors = ["Folkert de Vries"]
5+
date = "2026-06-16"
6+
7+
[taxonomies]
8+
tags = ["zlib-rs", "data compression"]
9+
10+
+++
11+
12+
As of [150.0.0](https://www.firefox.com/en-US/firefox/150.0/releasenotes/), Firefox uses zlib-rs for gzip (de)compression. This is very exciting, and has both performance and safety advantages.
13+
14+
We first started talking to Mozilla engineers in summer 2024, and it took 2 years to actually get zlib-rs into production. What took us so long?
15+
16+
<!-- more -->
17+
18+
## Integrating zlib-rs into the Firefox codebase
19+
20+
Switching to zlib-rs is not entirely trivial: we present zlib-rs as a drop-in compatible replacement, but there are some asterisks to this claim. We change the algorithms that are used at the different compression levels (in a way that is consistent with [zlib-ng](https://github.com/zlib-ng/zlib-ng), but inconsistent with stock zlib), so the exact output bytes and output length can change slightly.
21+
22+
The Firefox test suite tested for the exact output bytes in some cases, and for the (rough) output length in more. This is a good fail safe against messing up the compression configuration, but now these tests all needed to be updated.
23+
24+
Firefox also adds a prefix to all symbols: instead of `inflate` it uses `MOZ_Z_inflate` to prevent symbol clashes. We've long supported prefixing the symbol name in various ways, so getting this to work was just a matter of configuration.
25+
26+
So some work was needed, but the changes were straightforward. All seemed well, until...
27+
## Intel CPU bug
28+
29+
We started seeing [crashes](https://bugzilla.mozilla.org/show_bug.cgi?id=1950764). The logs showed that a bounds check had failed that logically couldn't fail. Of course, we're lucky that we even got a bounds check failure; in C you'd just get silent data corruption.
30+
31+
We could not reproduce the issue locally, and as more reports came in, a pattern started to emerge: our implementation triggered the infamous Intel Raptor Lake CPU bug.
32+
33+
This generation of CPUs is plagued by [instability and degradation issues](https://en.wikipedia.org/wiki/Raptor_Lake#Instability_and_degradation_issue). Something in our code was prone to triggering these issues, but of course we had no idea what, or even how to track it down.
34+
35+
Eventually Fabian Giesen wrote ["Oodle 2.9.14 and Intel 13th/14th gen CPUs"](https://fgiesen.wordpress.com/2025/05/21/oodle-2-9-14-and-intel-13th-14th-gen-cpus/), which identifies the problem as a particular instruction used in writing the result of Huffman coding to memory. Zlib also uses Huffman coding, and zlib-rs turned out to also use the offending instruction.
36+
37+
## Fixing the bug
38+
39+
Once you know what to look for, fixing the issue is reasonably straightforward. We had this function:
40+
41+
[https://godbolt.org/z/GjfYdPe3x](https://godbolt.org/z/GjfYdPe3x)
42+
43+
```rust
44+
pub fn push_dist(&mut self, dist: u16, len: u8) {
45+
let buf = &mut self.buf.as_mut_slice()[self.filled..][..3];
46+
let [dist1, dist2] = dist.to_le_bytes();
47+
48+
buf[0] = dist1;
49+
buf[1] = dist2;
50+
buf[2] = len;
51+
52+
self.filled += 3;
53+
}
54+
```
55+
56+
This code is dead simple: we assign three byte values to consecutive indices of an array. But the assembly for this function (with LLVM 22) has this move from `ch` to memory, which is bits 8-15 of the RCX register:
57+
58+
```assembly
59+
mov byte ptr [rsi + rdi + 1], ch
60+
```
61+
62+
Due to the hardware bug, occasionally this instruction will actually write bits 0-7 instead, causing the crashes we were seeing.
63+
64+
To work around LLVM emitting this particular instruction, we use a tiny bit of unsafe code (LLVM is clever, so this was the simplest way we've found to have it generate the right thing):
65+
66+
```rust
67+
pub fn push_dist(&mut self, dist: u16, len: u8) {
68+
let buf = &mut self.buf.as_mut_slice()[self.filled..][..3];
69+
70+
let bytes = dist.to_le_bytes();
71+
unsafe { buf.as_mut_ptr().cast::<[u8; 2]>().write_unaligned(bytes) }
72+
buf[2] = len;
73+
74+
self.filled += 3;
75+
}
76+
```
77+
78+
The fix in Firefox is [here](https://github.com/mozilla-firefox/firefox/commit/711ef51645a2#diff-945832833d688a990ab42ad9c84ce62a5258698d92bbcabbcdaabc2efbbda282). The patch has been [upstreamed](https://github.com/trifectatechfoundation/zlib-rs/pull/520) into zlib-rs and we will continue to carry that patch for the foreseeable future: it's a marginal amount of unsafe that is easily vetted. These are the sacrifices we make to run reliably on a variety of platforms.
79+
80+
It turns out that LLVM 23 no longer emits the offending instruction, although I believe that is serendipitous and not deliberate. When we bump our MSRV to a version that requires LLVM 23 (e.g. for custom allocators and c-variadic functions) we can drop this workaround.
81+
82+
## Results
83+
84+
So why go through all of this trouble? Because zlib-rs is faster. Much faster. Especially on linux x86_64 the speedup is almost silly. These benchmarks from [zlib-py](https://github.com/Rust-for-CPython/zlib-py) compare stock zlib versus zlib-rs:
85+
86+
```
87+
-------------------------------------------------------------------------
88+
ONE-SHOT DECOMPRESSION
89+
-------------------------------------------------------------------------
90+
Benchmark CPython zlib zlib_py Speedup
91+
-------------------------------------------------------------------------
92+
decompress 1 KB level=1 7.1 us 1.3 us 5.66x faster
93+
decompress 1 KB level=6 7.0 us 2.1 us 3.34x faster
94+
decompress 1 KB level=9 7.0 us 2.1 us 3.33x faster
95+
decompress 64 KB level=1 219.4 us 6.8 us 32.50x faster
96+
decompress 64 KB level=6 218.6 us 7.6 us 28.70x faster
97+
decompress 64 KB level=9 217.9 us 7.9 us 27.53x faster
98+
decompress 1 MB level=1 3.41 ms 128.0 us 26.61x faster
99+
decompress 1 MB level=6 3.42 ms 125.2 us 27.30x faster
100+
decompress 1 MB level=9 3.33 ms 134.8 us 24.71x faster
101+
decompress 10 MB level=1 33.95 ms 1.74 ms 19.50x faster
102+
decompress 10 MB level=6 33.94 ms 1.68 ms 20.16x faster
103+
decompress 10 MB level=9 33.80 ms 1.74 ms 19.42x faster
104+
```
105+
106+
```
107+
-------------------------------------------------------------------------
108+
STREAMING DECOMPRESSION
109+
-------------------------------------------------------------------------
110+
Benchmark CPython zlib zlib_py Speedup
111+
-------------------------------------------------------------------------
112+
stream decompress 1 KB L6 7.3 us 2.7 us 2.74x faster
113+
stream decompress 64 KB L6 221.3 us 22.7 us 9.75x faster
114+
stream decompress 1 MB L6 3.36 ms 309.0 us 10.86x faster
115+
stream decompress 10 MB L6 33.71 ms 3.79 ms 8.89x faster
116+
```
117+
118+
Compression is also faster, but harder to compare because the difference in compression ratio.
119+
120+
Via these benchmarks we noticed that the speedup is smaller on aarch64 systems, especially those running macOS. It turns out that Apple provides a more optimized zlib dynamic library, which uses inline assembly for some of the most performance-sensitive parts. This made us realize that there are some optimizations that we missed before, and we're now in the process of integrating them.
121+
122+
## Conclusion
123+
124+
Upgrading to zlib-rs should be straightforward, but in this case we encountered the toughest bug we've seen so far. With CPU bugs, there isn't much to go on, and our standard debugging tools are of little value. We spent months not really sure what to do, but now we have a workaround and can finally move forward.
125+
126+
We're very excited about zlib-rs now serving many more users. We want to thank Mozilla, and specifically Mike Hommey and Gabriele Svelto, for the integration work and tracking down and fixing the CPU bug.

content/news.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ template = "news.html"
55

66
[extra]
77
blogposts = [
8+
"zlib-rs in Firefox",
89
"Announcing Zstandard in Rust",
910
"Three years of Rusty sudo",
1011
"Tail calls project goal",

content/projects/zlib-rs.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ supporters = [
2828
]
2929

3030
blogposts = [
31+
"zlib-rs in Firefox",
3132
"Compression compiler contributions",
3233
"zlib-rs stable API",
3334
"Emulating avx-512 intrinsics in Miri",

0 commit comments

Comments
 (0)