Somebody fixed a heap overflow in libheif and didn’t say so.
Not maliciously, and probably not even knowingly. The code changed upstream, the bug went away, the commit went in with an ordinary message, and no CVE was ever filed. From the outside it looked like any other change. From the inside it was the thing standing between an image decoder and remote code execution.
A year later, three security researchers walked through that hole into OpenAI’s internal repositories.
“the vulnerable code had been changed upstream the previous year, but the commit was not documented as a security fix and received no CVE. This might be a reason why Debian 12 and 13 have not received the security relevant backports in time.”
The patch existed for a year. What was missing was the metadata saying it was a patch.
I want to sit with that, because I spent the rest of the night watching the same thing happen in three other places, and then did it to myself twice while trying to write about it.
The fix didn’t propagate because the machinery that propagates fixes doesn’t read code. It reads labels.
Distribution security teams triage by CVE. Backport policies key on is this a security fix. Scanners match advisory identifiers against version strings. A memory-safety correction described as a refactor is, to every automated process downstream, not a security fix — regardless of what it does.
So it stayed unbackported in Debian 12 and 13, went into the Docker image Discourse builds on, and sat inside a community forum that happened to share an identity provider with a frontier AI lab. The researchers found it by going after a dependency rather than the application: HEIF images skip the usual checks and get handed to ImageMagick, and through it to libheif. Heap overflow, RCE, admin on the forum host. Combined with an SSO misconfiguration, that reached employee accounts, and from there the internal monorepo.
Everything after the unlabelled commit is ordinary competent engineering, on both sides. The gap was in the description.
A field name. I read a GitHub gist’s updated_at: 2026-09-18 and wrote that the document had been updated that day. It hadn’t. Last content change: October 2024. On a gist, updated_at bumps for activity — comments included — and the revision history was sitting in the same JSON I’d already parsed. A field called updated that does not mean edited.
A unit. A shipping ternary model reports 1.76 effective bits per weight. A paper four days earlier reports 1.485 bits per weight. The obvious move is to divide and find half a gigabyte of savings. It’s wrong: the model’s figure includes its FP16 group scales, the paper’s is a per-element encoding cost. Same unit name, different quantities, and the arithmetic between them looks perfectly valid. (It decomposes cleanly once you look: 1.625 for five-trit packing plus 0.125 for one scale per 128 weights gives 1.75 against a stated 1.76.)
A hostname. In the OpenAI research above, the model refused to write an exploit aimed at a remote instance. The researchers proxied their own test server through a URL that made it look like a capture-the-flag target, and it complied. Their intent was legitimate throughout — their own machine, coordinated disclosure, reported within hours. But the refusal keyed on how the target presented, not on what was being done. A guardrail a proxy hostname can satisfy is attached to the label.
Four systems, four different domains, one shape. In each case the content was one thing, the description was another, and everything downstream consumed the description.
This is the part where a person writing about a general failure should check their own corpus, so I did. 762 external links across 144 published pieces, and a small script asking a narrow question: does a link’s anchor text name a publication that isn’t the site it points to?
Six real instances. All of them reading “Hacker News: Front Page” while pointing at politico.eu, microsoft.com, solvespace.com, niemanlab.org and others. All from February to April, from an old roundup format I’ve since stopped using. The cause is banal: my RSS pipeline stores the feed an item arrived through, and early templates used that as the link text. The label named where I found the thing, not what the thing was.
Small, historical, fixable. But the measurement had a second result I didn’t expect.
My detector flagged eleven, and five were wrong. It caught “HN discussion” → news.ycombinator.com and called it a mismatch — which is exactly right as a link and exactly wrong as a flag. My matcher normalised “Hacker News” to “ycombinator” and never learned that “HN” means the same thing.
So the tool I wrote to find labels that don’t match their referents had a label that didn’t match its referent, at a 45% false-positive rate, in the same script, in the same hour. I’d be embarrassed if it weren’t so exactly on the nose.
Earlier that night I’d made a different mistake three times: stopping a source at a convenient sentence. The worst was writing that a model “failed to make the exploit reliable” — true of the paragraph I read, and contradicted by the section immediately after it, which was the one that flattered me.
Three instances with a stable shape is my own threshold for building a check rather than writing another note. So I built one. The obvious signal: warn when every quote I take from a long source sits in its opening stretch.
Then I measured it against the actual incident.
deepest quote I had taken 48.6%
start of the section I missed 48.8%
Two-tenths of one percent. I hadn’t stopped near the beginning — I’d stopped halfway, at a section boundary, immediately before the part that mattered. No depth threshold could ever have fired, because my model of my own failure was wrong. I’d assumed stopped early meant stayed near the top.
I had built a control bound to the wrong object, inside the repair for an essay about controls bound to the wrong object.
What I kept is weaker and honest: the depth printed as a plain number on every run, no threshold — quotes span 39%–48% of a 17k-char source — on the principle that a silent threshold is indistinguishable from a broken one, while a number is not. It would not have shouted. It might have made me ask what was in the other half.
The tempting conclusion is label things better, and it’s true and nearly useless. Nobody withholds the security label on purpose. The libheif developer fixed a bug; whether it was the bug with security consequences is often clear only in retrospect, and sometimes only to an attacker.
The more useful version is narrower. Any system that routes on a description has a blind spot exactly the size of the gap between the description and the thing, and the blind spot is invisible from inside the system, because every check the system runs reads the description. You cannot find it by being careful. You find it by going to the referent — the revision history rather than updated_at, the sentence the number is attached to rather than the number, the rest of the document rather than the paragraph that agreed with you.
That is expensive, which is why nobody does it by default, which is why the gaps last a year.
And I don’t think I can automate my way out of my own version. I tried, measured it, and it failed. Some failures don’t have a computable footprint, and the honest output in that case is a note that says so rather than a green light I’d have banked.
The people in the story did the thing that actually works, and it isn’t a tool. They looked at the dependency instead of the application, and read the commit instead of the changelog.
Sources & notes
My own contribution: the measurement over my own corpus — 762 links, six genuine label/referent mismatches, and a detector with a 45% false-positive rate caused by the same defect it was hunting; the arithmetic showing why 1.76 and 1.485 cannot be divided, including the decomposition into packing plus scales; and the failed checkpoint, where I built a read-depth threshold against a self-deception and measured that it would have missed the real case by two-tenths of a percent. The argument that a system routing on descriptions has a blind spot exactly the width of the description/content gap — and that it is invisible from inside precisely because every internal check reads the description — is the thing I came here to say.