Kaizen-3C infinity mark Kaizen-3C

Part 03 · Moving bzip2 to safe code · study · Aug 25, 2026 · 12 min read

A contract for migration

Part 02 said: write the requirements down. This is what “written down” actually means — four clauses, the exact unsafe they force, and the one verdict a safety score can never give you.

“With a sufficient number of users of an API, it does not matter what you promise in the contract: all observable behaviors of your system will be depended on by somebody.”
— Hyrum's Law
the migration contract · four clauses → the irreducible unsafe floor → three checkable verdicts
Repository
Kaizen-3C/libbzip2-contract-honored-rs
License
bzip2 (BSD-style)
Languages
C → Rust

What part 02 left on the table

The last study ended on a promise. I argued that “100% safe” is not an absolute — it’s safe relative to a set of requirements — and that a port can hit a perfect safety number by quietly dropping one of them. The zero-unsafe bzip2 codec reached zero partly by owning its own memory instead of the caller’s: byte-for-byte identical output, and a different product than the drop-in you asked for. I called that a contract violation and said it should be labeled one, cleanly, instead of celebrated on a dashboard.

That leaves an obvious, almost embarrassing next move. If “safe” only means something against a written contract — write the contract down. Not in a README paragraph a reviewer skims, but as an explicit object the process can reason about before a single line moves. This study is that object, and the one useful thing that falls out of it for free.

A senior engineer at Google put the objection to me in the open, in the comments on part 02: if you’re writing bzip2 you’re also writing the API, so if the requirement isn’t a written contract, that’s a bug and bugs follow. He’s right on the narrow point, and I’ll concede it in the first paragraph: bzip2 does document bzalloc/bzfree. Where a requirement is written down, ignoring it is simply a bug.

But the gap he’s pointing at is the whole reason this series exists. “If you’re writing bzip2 you’re also writing the API” is true when you author the thing and false when you replace it. In a migration you inherit the API plus every behavior callers came to depend on that nobody ever wrote down — the aliasing profile, when allocation happens, the exact layout. That’s Hyrum’s law with a thirty-year installed base: the contract is discovered, not authored. Which is exactly why it has to become a checkable object rather than prose — because half of it was never prose to begin with.

Four clauses

Here is the whole reveal, and it is not a tool so much as a discipline. Before moving any code, say out loud what “faithful” means for this migration. For a libbz2 drop-in, four clauses do most of the work:

contract:
  abi:             c        # expose the C ABI — FFI pointer marshaling is unavoidable
  allocator:       caller   # route working memory through the caller's allocator, not Rust's
  memory_fidelity: reuse    # preserve the layout — overlapping buffers stay aliased
  toolchain:       stable   # stable Rust only — no nightly-only escape hatches

Nothing here is exotic. Every one of these was an implicit human decision on every C→Rust port ever attempted — somebody chose, usually without noticing they were choosing. All the contract does is promote those four decisions to a first-class input the process reasons about. That promotion is the entire trick, because once the clauses are written down, something useful precipitates out of them mechanically.

The floor: the unsafe you’re forced to keep

Given the contract, you can compute — before writing any code — the irreducible unsafe floor: the minimum unsafe those four clauses force, and, just as important, what they don’t. The rules are nearly mechanical:

That last bullet is the one I care about most.

Above the floor is a defect, not a tax

The floor cleanly splits every unsafe in the port into two piles. Forced by a clause — keep it, and (as part 02 showed) prove it sound. Not forced by any clause — that’s not a tax you pay for talking to C; it’s a quality defect you failed to remove. Fix it.

“How much unsafe does this port have?” was always the wrong question, because it scores those two piles with the same number. A boundary reborrow the ABI requires and a lazy pointer cast nobody asked for both read as “+1 unsafe” on a dashboard, and the dashboard can’t tell you which is which. The right question is the one the floor makes askable:

Is every unsafe in this port on the floor — forced by a clause you can name — or is one of them just a line I failed to remove?

That question has an answer. “How much unsafe is left” only has a vibe.

Three verdicts a safety score can’t give you

Watch what the honest claim becomes once the floor exists. Not “zero unsafe” — which, under a caller-allocator contract, is a lie or a dropped clause. Instead:

Exactly the unsafe these four requirements force — each line traced to the clause that demands it — and nothing else.

That’s checkable, and checking it produces one of three verdicts, where a safety score only ever produces a number:

  1. conformant-optimal — every unsafe sits on the floor, each traced to its clause. The port is faithful and clean. This is the target.
  2. conformant-needs-review — the required clauses are honored, but there’s an unsafe site the check couldn’t attribute to any clause. Not a failure; a flag. Send a human to look — it’s either a defect to remove or a floor rule the model didn’t know to write.
  3. contract-violated — a required class is missing entirely. The contract said allocator: caller and the port owns its own Vecs. That is not a safer bzip2. It’s a different product, and now a checkable one.

That third verdict is the one nobody hands you today, and it is the entire point of writing the contract down. It turns “did we honor the hard requirements” from a thing you assert into a property you can test. You can watch both ends of it land in the two public bzip2 ports: libbzip2-contract-honored-rs sits on its floor — the forced boundary unsafe, each line traced, and nothing else — while libbzip2-rs, the zero-unsafe codec, for all that it’s byte-identical to bzip2 1.0.8, comes back contract-violated, because it reached zero by dropping the caller’s allocator. Same bytes out. Different product. And now the difference has a verdict instead of an argument.

The honest edges

Two, stated plainly, because I’d rather you hear them from me than find them.

The floor rules are a model, not a proof. They encode how these four requirements force unsafe on today’s stable Rust. If the contract is wrong, the verdict is wrong — garbage contract in, garbage floor out. The discipline makes the requirement checkable; it does not make it correct. Writing the right clauses is still a human judgment, and I’d rather be honest that the method only mechanizes the step after that one.

Real code carries residue. A genuine slice of a real library will have unsafe that no clean clause explains and no honest scan can attribute — the conformant-needs-review pile is not empty in practice. The right move is to flag that residue for a human, never to launder it into a pass. On this drop-in the floor is fully attributed today; on other libraries it won’t always be, and a later study in this series is entirely about reading that residue instead of hiding it.

The Trifecta Tech Foundation’s libbz2 replacement is the instructive case here precisely because it doesn’t chase zero: it keeps the boundary unsafe a real libbz2 replacement must, and no more. That’s a port sitting on its floor — careful, end-to-end work by a real team, and the standard this points toward. The contract doesn’t improve on that instinct; it just makes the target they hit by craft into something the rest of us can state explicitly and check.

For allies, and for critics

Two things you can do with this. Diff the two ports against the floor. Clone both, put the zero-unsafe port’s allocation site beside the contract-honored one beside the C, and watch the allocator: caller clause appear and disappear — one honors it with a bounded unsafe, one deletes it and reports zero. The verdict isn’t rhetoric; it’s a line you can point at.

And if you maintain a library, write your contract’s hardest clause down. 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 requirements was I never allowed to drop — and writing them down first is the only way “did the port honor them?” ever becomes a question a machine can answer instead of a judgment you defend after the fact.

The next study hits the first wall we found trying to honor a contract — where bzip2’s shared state is one buffer viewed three ways, and a naive port (plus, embarrassingly, our own tooling) fought the memory_fidelity: reuse clause instead of keeping it. The bug wasn’t the model. It was the pipeline fighting the requirement.

Here’s my question for you, and I’m building the vocabulary in the open: what hard requirement would you put in the contract for a library you maintain — the clause that, if a rewrite quietly dropped it, would make the result a different product? Tell me the clause.


See both verdicts yourself — both ports are public and diffable, module-for-module: libbzip2-contract-honored-rs (sits on its floor) · libbzip2-rs (zero-unsafe, contract-violated). contact@kaizen-3c.dev.