When I introduced afmpeg, one phrase did a lot of heavy lifting: safely process untrusted media. The FFmpeg inside is compiled to WebAssembly and run in a sandbox with no host filesystem, no network, no environment. Feed it a hostile file and the worst it can do is fail. That was the pitch.
An external security review read that pitch, then asked a better question than I had: fail taking how much of the host with it?
Two different questions wearing one word
“Sandboxed” answers one of them: what can it reach? And on that score afmpeg was solid. The guest sees an in-memory filesystem it was handed and nothing else. A malicious file that manages to run code inside the guest has, for all its scheming, broken into a padded room. No door, no window, nothing worth taking.
But there’s a second question the word “safe” smuggles in without telling you: how much can it take? Here afmpeg had no answer at all, and that was the review’s headline finding, one I’d commissioned and then made a point of verifying against the code rather than taking on trust. The runtime set no memory limit. None. A wasm32 guest can grow its linear memory towards 4 GB, and a crafted media file is only too happy to help it get there: declare a 65535×65535 video in the header and libavcodec, doing exactly the job you asked of it, tries to allocate the buffers those dimensions demand. Inside the sandbox. Paid for by the host.
So the guest can’t read your files, can’t phone home, can’t escape… and can still get your Go process OOM-killed by the kernel. I’d sandboxed the code execution and said nothing whatsoever about the resource bill. My padded room, it turned out, was drawing its air from the rest of the building, and I hadn’t thought to check.
The second finding was the same disease with a different clock: nothing bounded how long an invocation could run. So a pathological decode loop, handed a caller using a background context, would sit on the runtime’s lock forever. Not a crash. A wedge. (There was a third one too, minor: some missing type guards in the engine’s job-spec parsing, defence-in-depth against a caller the design already trusts. Fixed in the engine for completeness.)
Ceilings, on by default
The fix (0f20cbb) gives the sandbox the second half of its job. Memory first:
rtCfg := wazero.NewRuntimeConfig().
WithCoreFeatures(runtimeCoreFeatures).
WithCloseOnContextDone(true)
if pages := memoryLimitPages(cfg.memoryLimitBytes); pages > 0 {
rtCfg = rtCfg.WithMemoryLimitPages(pages)
}
The default ceiling is 512 MB, generous for real transcodes and a rounding error compared to “all of it”. A guest that tries to grow past the ceiling gets a failed allocation inside the sandbox, which FFmpeg handles the way it handles any allocation failure: the job errors, the host shrugs, life continues.
Time second (same commit):
// Impose the default deadline before locking, but only when the caller brings
// none — a caller's own deadline is honoured as-is (spec 0027 §4B).
if _, ok := ctx.Deadline(); !ok && r.timeout > 0 {
var cancel context.CancelFunc
ctx, cancel = context.WithTimeout(ctx, r.timeout)
defer cancel()
}
An hour by default, and it steps aside for whatever deadline the caller already set. Both knobs can be turned, and setting either to zero opts out completely, but the decision that actually matters is the one in the spec’s own words: a sandbox whose protections are off-by-default is not a sandbox. The library user who never opens the hardening docs, which is most of us, most of the time, me being exhibit A, gets the ceilings anyway. Anything you have to remember to switch on is a thing you’ll forget to switch on.
Found on paper, not in production
For the record, nothing OOM’d in anger. This gap was found by a review I paid for, corroborated line-by-line against the runtime before a word of spec got written, and closed the same week. That’s the least dramatic possible version of this story and exactly the version I wanted: the alternative draft, the one where a user’s server dies processing a wedding video, writes itself and is worse in every respect.
Still, I’d rather own the embarrassing half out loud. I shipped “safely process untrusted media” while the runtime would happily hand a hostile file every byte it asked for, because I’d let the word “sandboxed” answer both questions when it only ever answered one. What can it reach? Fine, covered. How much can it take? Not a clue. A sandbox has to answer both before that little phrase is true, and mine was answering half of it with a completely straight face.





