Skip to main content

The Double-Free and the Sized Integers

· 6 min read

Someone was writing a graceful shutdown handler for an HTTP server. They called server.close() at the right moment, the way you’re supposed to. Then their actor crashed, and nothing in the crash pointed at the http module.

That’s what found the double-free.

How the double-free worked

http.Server, regex.Pattern, http.Request, and json.Value all had impl Drop registrations. Drop called the same underlying free function as the explicit .close() / .free() methods. So if you called the explicit method — the obvious, deterministic thing to do — the type would go out of scope afterward and Drop would fire again on an already-freed allocation.

The crash was essentially untraceable at the point it surfaced. The actor runtime was tearing down after a normal scope exit, a free happened to land on a slot that had already been reclaimed, and the error manifested somewhere unrelated. There was no indication it was http. (The fix turned out to be a one-line deletion. Getting to that line took longer.)

let server = http.Server.bind(":8080");
// ... handle requests ...
server.close();  // explicit close
// impl Drop fires here on exit — double-free, crash elsewhere

The fix was to remove impl Drop entirely from all four types — no implicit release on scope exit, meaning you call .close() or .free() explicitly at every exit point, or things leak. That’s a worse default in one sense — a forgotten call leaks instead of double-freeing — but it’s a tractable problem. A leak is detectable and deterministic. A double-free is neither. This landed across the http.Server / regex.Pattern cleanup in #1314 and the http.Request / json.Value cleanup in #1500.

let server = http.Server.bind(":8080");
// ... handle requests ...
server.close();  // only teardown path — no Drop

The migration is mechanical: find every place one of these types exits scope and add the explicit call. The migration guide has the patterns.

Why the stdlib had sized integers everywhere

Hew has one integer type for most purposes: int. It’s the type you use for counts, indices, lengths, loop bounds. There’s no reason to reach for a sized variant unless you’re working at an ABI boundary or representing a wire format.

I spent years writing Rust before building Hew, and the reflex of reaching for i32 or i64 for “integer-y things” is apparently deeply ingrained — the stdlib got written with i32 lengths and i64 timestamps all over it. Not because there was a reason. Just habit.

By the time it was obvious this was wrong, the pattern was across 22 modules. Every stdlib call that took or returned a sized integer forced user code to write casts that communicated nothing:

// Before v0.4.0
let n: int = items.len();
let chunk = items.get(n as i32);  // why is this as i32?

The cast isn’t documenting a real constraint. The underlying implementation doesn’t need i32 specifically. It’s just carrying forward a type error from when the function was first written.

After fixing the 22 affected modules in #1218:

// After v0.4.0
let n: int = items.len();
let chunk = items.get(n);

Because this change touched public signatures, it was compile-time-blocked — the compiler refused to build until every affected call site was updated. That turned out to be useful. There were about 140 affected sites in the test suite alone, spread across files that were easy to forget existed. The build error list was the audit.

The one category of code that wasn’t touched: extern "C" blocks and anything annotated // INTERNAL-ABI:. Those have their own type contracts, and imposing int on them would break the ABI semantics they’re explicitly managing. The cleanup stops at the public API boundary.

I don’t have a good explanation for why it took until v0.4.0 to fix this. The problem was visible early. I think I kept deprioritizing it because it wasn’t blocking anything immediately, and the casts were easy enough to write. Then there were 140 of them and it became a project.

Hew Notes

Language development, engineering articles, releases and community projects. About once a month, with an extra issue for a significant release. Written in English.

Subscribe to Hew Notes