Kaizen-3C infinity mark Kaizen-3C

Part 02 · Moving bzip2 to safe code · study · Aug 11, 2026 · 13 min read

The 0-unsafe trap: when zero means you changed the spec

Two ports of the same bzip2, byte-for-byte identical, both safe — and only one is the drop-in you asked for. What the difference is made of, and how to prove the unsafe you keep is sound.

“There are two ways of constructing a software design: one is to make it so simple that there are obviously no deficiencies, the other is to make it so complicated that there are no obvious deficiencies.”
— C. A. R. Hoare
contract-honored drop-in · retained boundary unsafe proven Miri-sound (Stacked + Tree Borrows, single-block)
Repository
Kaizen-3C/libbzip2-contract-honored-rs
License
bzip2 (BSD-style)
Languages
C → Rust

Two ports, one question

Here are two Rust ports of the same bzip2 function. Both compress and decompress your data and hand you back exactly the bytes you started with — proven, byte-for-byte, by the same differential harness. One keeps a small amount of unsafe. The other is #![forbid(unsafe_code)] — zero unsafe, top to bottom.

Which is the better migration?

The reflex answer is the zero-unsafe one. Zero is a nice number; a dashboard loves it. But “how much unsafe” is the wrong question asked with confidence, and this study is about the right one — because on this particular function, the zero-unsafe port is safe the way a car with the engine removed is safe. Yes. And also it’s not the thing.

What a drop-in actually owes the caller

A drop-in replacement isn’t defined by “same output.” It’s defined by same contract — everything a caller was allowed to rely on. bzip2’s C API writes three of those requirements down in plain sight.

1. The caller’s allocator. BZ2_bzCompressInit takes two function pointers and expects the library to route all its working memory through them:

typedef struct {
   /* ... */
   void *(*bzalloc)(void *, int, int);   /* the caller's allocator  */
   void  (*bzfree )(void *, void *);      /* the caller's free       */
   void  *opaque;                         /* the caller's context    */
} bz_stream;

This is not a vestige of old C. A caller might be an embedded arena, a custom pool, a language runtime with its own heap and its own idea of where bytes may live. “Use my allocator” is a promise the library made.

2. The memory layout / aliasing. bzip2’s sorting core allocates one block and views it through several pointers at once — the same bytes as words here, as raw bytes there, as an offset tail elsewhere. Those views alias on purpose; the algorithm is built on it.

3. The C ABI. The exported symbols, the #[repr(C)] struct, the calling convention — so an existing C program can link the new library without recompiling the world.

How the zero-unsafe port gets to zero

Give a well-meaning translator “port this, and get unsafe to zero,” and watch what it does with requirement #1. There is no safe way, on stable Rust, to own memory that came from someone else’s allocator — a raw pointer into caller memory is exactly the case unsafe exists for. So to reach zero, the port stops using the caller’s allocator and allocates its own:

// The zero-unsafe port — owns its memory. Look, no unsafe!
let mut buf: Vec<u8> = vec![0; n];         // Rust's global allocator, NOT the caller's

// The contract-honored port — honors the allocator the caller handed us. One small, bounded unsafe.
let raw = unsafe { (strm.bzalloc)(strm.opaque, 1, n as i32) };   // the caller's function pointer
let buf = unsafe { core::slice::from_raw_parts_mut(raw as *mut u8, n) };

The first compiles, looks idiomatic, and reports zero unsafe. It also silently deleted requirement #1. The embedded caller with the custom arena now has bzip2 quietly calling the global allocator behind its back. The number went to zero by changing the spec.

Both ports are published and diffable, module-for-module: libbzip2-rs is the owned-Vec one — the zero-unsafe codec you’d reach for when the requirements genuinely allow it — and libbzip2-contract-honored-rs keeps the caller’s allocator, the aliased layout, and the ABI, retaining only the bounded unsafe that forces. Run make check against either and it’s byte-for-byte identical to bzip2 1.0.8. Duck typing in the wild: both walk and quack like bzip2 from the outside; only one is the drop-in you actually asked for.

The number you want isn’t zero

Here’s the uncomfortable idea the whole series turns on: “100% safe” is not absolute — it’s safe relative to a set of requirements. If you never wrote the requirements down, “safe” can always be bought by quietly dropping one of them. Own the memory instead of borrowing it. Copy instead of alias. Widen a type here, drop a callback there. Each move deletes an unsafe and, sometimes, deletes the point.

So the number you want isn’t zero. It’s exactly the unsafe the requirements force, and not one line more — and then a proof that what you kept is sound. Notice what the people who know bzip2 best did for the drop-in: the Trifecta Tech Foundation’s libbz2 replacement keeps precisely the boundary unsafe a real drop-in must, contained and deliberate — a genuine piece of engineering and the standard this points toward. They refused to change the spec to hit a number.

Proving the unsafe you keep

Keeping unsafe is only honest if you can show it’s sound — that the retained boundary code has no undefined behavior, not just that it produces the right bytes today. Byte-identity can’t see UB: a program can round-trip perfectly and still be one optimizer pass away from disaster.

So the contract-honored port carries a second proof next to the differential. make miri runs the retained unsafe under Miri over a path-covering corpus, under both aliasing models — Stacked Borrows and the stricter Tree Borrows. That pass was not a formality. It caught four real latent UBs that make check was completely blind to, each fixed output-preservingly (the details are in the repo’s SOUNDNESS-CERT.md):

None of those changed a single output byte. All of them were genuine undefined behavior. That’s the point of a soundness receipt: it proves something a byte-for-byte test cannot — and it re-runs on your machine.

The honest edge: single-block

Here is the caveat I’d rather state than have you find. The contract-honored drop-in is proven byte-identical and Miri-sound over single-block inputs — the envelope the harness covers. It is not yet proven over multi-block streams (larger than ~100 KB at low compression levels, where bzip2 splits the input into multiple blocks). That’s a real boundary, the repo says so in its own README, and it’s the current edge of the work — not a footnote I’m hoping you skip. A contract you honor over one regime and haven’t proven over another is exactly the kind of thing this whole series exists to label honestly.

Write the contract down first

The fix is boring and, I think, correct: capture the requirements as an explicit contract before moving any code — the C ABI, whose allocator, whether the memory layout must be preserved, stable toolchain or not. Once it’s written down, you can ask a question that “is it safe?” can’t answer:

Did this port honor the requirements, or did it hit its safety number by dropping one?

That question is checkable, and it has a clean verdict. A port that keeps its own Vecs under a “use the caller’s allocator” contract isn’t a safer bzip2 — it’s a contract violation, and it should be labeled one, cleanly, instead of celebrated on a dashboard. The zero-unsafe codec isn’t a failure; it’s a perfectly good answer to a different contract (one where owning memory is allowed). The trap is only when it’s scored as if it answered yours.

For allies, and for critics

Two things you can do with this. Clone both ports and diff them module-for-module — put the zero-unsafe decompress.rs beside the contract-honored one beside the C, and watch the allocator requirement appear and disappear. And if you build FFI-boundary Rust, make miri under Tree Borrows is worth stealing regardless of what you think of bzip2: it finds the aliasing sins that pass every functional test you have.

The received wisdom is that moving C to Rust is a transliteration problem — get the syntax across, chase the unsafe to zero. That skips the actual question. The real one isn’t how do I remove the unsafe. It’s which unsafe was I never allowed to remove — and can I prove I kept only that, and that it’s sound.

The next study writes the contract down as an explicit object — the allocator, aliasing, and ABI clauses — so “did you honor it?” stops being a judgment call and becomes something a machine can check.


Diff the two ports yourself — both public, both built to be picked apart: libbzip2-rs (the zero-unsafe codec) · libbzip2-contract-honored-rs (the contract-honored drop-in, Miri-sound over its single-block envelope). contact@kaizen-3c.dev.