investigate.blog Unnecessarily thorough

Analysis supply chain · 2026-11-05

A Backdoor With No Backdoor In It

Auditing the asterisk collision with xz eyes.

I spent an unreasonable amount of time reading the xz post-mortems. Not because the payload was clever. The payload was fine. What kept me up was the tradecraft. Two years of trust building. Test fixtures that were actually binaries. A release tarball that quietly diverged from the git repo. Sockpuppet accounts pressuring a burned-out maintainer to hand over release authority. It was a spy operation executed against a compression library, and it nearly worked.

Then the asterisk thing showed up on Hacker News and I couldn't stop thinking about it. Not because it resembles xz. Because it's the opposite shape, and it might be worse.

The target is root on Alpine's base image, which is the OS under a very large fraction of the world's containers. That is a bigger prize than anything xz touched. And there is no malicious line of code anywhere in the chain. No obfuscated fixture. No build-time trigger. No fake identity. Four separate projects, four separate sets of maintainers, four separate commit rationales that all read as responsible engineering.

Which raises the question I actually care about. If someone wanted this outcome, would we be able to tell? So I put on the xz lens and went through it layer by layer, assuming every stated rationale is good faith but scrutinizing every rationale anyway, because root access is on the other side of the door.

1. Why This Deserves the xz Lens

xz failed because of a detail nobody expected to matter. Andrei Freund caught it through an unrelated valgrind investigation where a 500ms delay showed up in SSH logins. The backdoor worked. The tradecraft worked. It was undone by an anomaly in timing that had nothing to do with the payload.

That story teaches a specific lesson. Sophisticated supply-chain attacks are detectable because they leave residue. Obfuscation is residue. Identity fakery is residue. Tarball divergence is residue. Build scripts that fetch remote payloads are residue. Every trick in the xz playbook left something to find.

The asterisk chain leaves nothing to find. It is deniable by construction. Every individual change survives review, and survived it, for fourteen years. If this was intentional, then it's not merely as good as xz. It's better, because it defeats the exact investigative technique that caught xz. You cannot find residue where there is no residue.

That's worth taking seriously. So let's take it seriously.

2. The 2013 Thread Was a Beacon

Most writeups treat the 2013 libc-alpha discussion as background. Proof that people were "aware" and "debated it." Through the xz lens it looks like something more specific. It's a public, indexed, permanently citable confirmation of three facts at once:

A Debian developer showed up to ask glibc to copy the behavior. Sit with that. From the perspective of someone paying attention, this costs nothing and gives away everything. You don't file a bug. You don't leave a trail. You don't write a line. You read. The thread didn't create the collision. It announced that the collision existed and that the community was treating it as a design argument rather than a vulnerability. That is the most comfortable place a collision can sit, because nobody is looking at it as a bug.

In xz, the attacker had to manufacture trust from nothing and sustain it for two years. Here, the trust already existed. The collision was already documented. The argument about whether it was a problem had already been had, and the answer was "it's philosophy." All a patient observer has to do is know where to wait.

3. Four Layers, Audited as if Each Was Deliberate

Here is the chain with the rationales removed and only the actions kept. I've annotated confidence in each layer's adversarial plausibility, because pretending all four are equally suspicious is its own kind of dishonesty.

Layer Action (rationale ignored) Adversarial read Confidence
musl, 2012 Error path returns the string "*" instead of NULL. Picks a sentinel that collides with a live authentication token, and explicitly declines to bury the translation in a wrapper where callers wouldn't see it. weak
musl, 2012 SHA-crypt rejects keys beyond 256 bytes, routing them into the error path. Makes the sentinel attacker-triggerable with one crafted password. The specific limit value is arbitrary. strong
OpenSSH, 2016 Hashes the supplied password against a real hash even for locked accounts, then compares strings. Creates the comparison surface. A sentinel returned from crypt() can now be compared against a stored sentinel. strong
Alpine, 2022 Root's lock marker moves from "!" to "*". Points the entire chain at the highest-value account, on the most-deployed container base image, using the one character musl emits on error. open

Layer two is where it gets interesting

The DoS rationale for the length limit is legitimate. SHA-2 crypt is superlinear in key length and unbounded input is a real problem. But the specific number isn't load-bearing for that rationale. 256, 300, 1024, all of them bound the work. Only one of them interacts with anything else in the system, and only if some other component's buffer, sentinel, or comparison happens to care.

Choosing 256 isn't suspicious on its own. Choosing 256 and then having the failure mode be "return the collision character" is where two commits twenty days apart compose into a remotely triggerable condition. If I were constructing this deliberately, that's precisely the shape I'd use. Two commits, each defensible alone, whose composition nobody reviews because reviews happen per commit.

Layer four is where it gets uncomfortable

The Alpine change is the one I keep returning to, because of what it is for. It's a usability regression fix. "!" broke SSH key auth on non-PAM systems, and "*" restores it. Entirely reasonable. But look at the shape. The only sentinel that both locks the account and keeps keys working is the one musl returns on failure. The change isn't merely compatible with the bug. It is constrained to be compatible with the bug by the surrounding system.

You cannot fix Alpine's usability problem with any other character without also changing musl. That is an extraordinary amount of leverage for one libc's error string to hold over a distribution's root account. In a deliberate operation, you would not need to touch Alpine. You would only need to know that when the usability regression arrived, the collision was the only available answer.

4. Why This Is Arguably Superior to xz

I want to make the comparison explicit, because I think it's the reason this case is worth writing about.

xz required a human being to sustain a lie for two years. Jia Tan had to build credibility, contribute code, pressure a maintainer, and eventually ship a payload. That is a lot of surface area. Every message, every commit, every release, every interaction was a place where the story could break. It broke because someone noticed a timing anomaly while doing something else entirely.

The asterisk collision requires nobody. If an adversary recognized the collision in 2013 and then simply did nothing, four other projects would eventually walk the system into a state where root on Alpine was reachable with a long password. No payload to catch. No identity to unmask. No tarball to diff. The operation would be indistinguishable from ordinary software evolution because, in every operational sense, it would be ordinary software evolution.

The xz attacker had to survive scrutiny. The asterisk attacker, if one exists, only has to survive patience. And there is no valgrind for patience.

5. What a Real Backdoor Leaves Behind

Here's the part that keeps me honest. I keep landing on "probably an accident," and not because the rationales are convincing. It's because xz left a completely different forensic profile and this chain doesn't match it.

Signal xz backdoor The asterisk chain
Obfuscated payload Yes. Binary test fixtures hiding a build-time trigger. No. Every line is readable C.
Concealed build step Yes. Release tarball diverged from the repo. No. Standard builds.
Identity manipulation Yes. Sockpuppets, sustained pressure, a multi-year trust campaign. No. Established maintainers, real names, long public histories.
Miscategorized intent Yes. Commits described as tests or cleanup. No. Commit messages describe exactly what the code does.
Concentration of authorship Yes. One actor across several "unrelated" contributions. No. Four projects, four sets of authors, no overlap.

The honest summary is that if this was a backdoor, it's a backdoor with no backdoor in it. Every element is a legitimate, reviewed, documented change that a security-conscious reviewer would have approved. There is no line you can point at and say "this has no innocent reading."

Which is either exculpatory or terrifying, depending on how you squint.

6. The Inversion: What Would an Adversary Have Done Differently?

This is the question I think is worth keeping. If you wanted this vulnerability and you had xz-level tradecraft, what would your moves be?

You would not write a malicious commit. You would select for a sentinel collision that a standards body was already arguing about, because the argument itself is your cover. It reframes the behavior as a philosophical disagreement instead of a bug.

You would not introduce a length limit. You would let someone else's DoS mitigation route attacker input into the error path, and you would be perfectly content that the limit's value looks arbitrary, because arbitrary values are unremarkable.

You would not touch sshd. You would wait for a timing-attack fix that adds a hash computation for locked accounts, because that fix is so obviously correct that nobody will ever ask what else it did.

You would not file the Alpine bug. You would wait for a usability regression that forces the distro to pick a lock marker, and you would be quietly pleased that only one character satisfies both constraints.

And then you would do nothing. That is the whole operation. Four other people do the work. Your only contribution is patience plus a mental model of how the layers compose.

The uncomfortable finding: an adversary using this strategy would leave roughly the same artifact trail we actually have. That doesn't mean an adversary did it. It means the artifact trail cannot distinguish between the two hypotheses, and we should stop treating "there is no malicious code" as a clean bill of health.

7. Where the Theory Breaks Down

I want to be clear that the steelman doesn't survive everything.

The 2013 thread cuts both ways. Yes, it's a beacon. It's also fourteen years of public discussion during which nobody weaponized it, and during which a different libc picked "*0" specifically to dodge the collision. If the collision were being shepherded, I'd expect at least one attempt to steer implementations toward the colliding value. I don't see one. The "*0" adoption is honestly the strongest single piece of evidence against intent, because it proves the collision was avoidable, was known to be avoidable, and was avoided elsewhere by people with no reason to coordinate with anyone.

Alpine's constraint is softer than it looks. The choice between "!" and "*" is constrained by sshd's allowed_user() logic, but that logic could have been changed. "Only one character works" is true given the surrounding code, not true in principle. A motivated actor could have patched sshd instead. Nobody did, which is consistent with both hypotheses.

There is no author overlap. This is the biggest problem for the theory. A patient single actor needs a position in at least one of these projects to steer anything. Four independent maintainers converging on a catastrophe is the ordinary explanation. One actor steering four projects over fourteen years is an extraordinary one, and extraordinary claims want at least one anomalous commit, mailing-list presence, or funding trail. I looked. I didn't find one.

8. What Survives

Strip out what the evidence won't support and you're left with a weaker claim that is, I think, more useful:

The asterisk chain demonstrates a capability. Somewhere in the design space of independently reasonable changes that compose into root access, this specific path exists. It was publicly documented for over a decade. It required no malicious code to create. Whether or not anyone intended it, we now know the shape of a backdoor that leaves no backdoor-shaped evidence.

That isn't a conspiracy theory. It's a threat model. And it's more actionable than either "it was an accident" or "it was sabotage," because it tells defenders what to look for without requiring them to establish motive first.

What to actually do with this

9. Closing

So no verdict. The evidence supports capability, not intent, and I'd rather leave that open than close it with a guess. What I'll say instead is that if this was an accident, we got lucky in the worst possible way. We now know the shape of an operation that would have worked, and we only found it because someone happened to hit a 256-byte limit.

But the xz lens taught me something I didn't have before. The absence of a payload is not the absence of an attack. It is, in the worst case, evidence of a better one. If a state actor or a patient individual recognized this collision in 2013 and simply waited, we would be looking at exactly what we're looking at now. That's not a claim about what happened. It's a claim about what we can rule out, and the answer is: less than we'd like.

Fail loudly. Namespace your sentinels. And when four reasonable decisions line up pointing at root, treat the alignment itself as the finding, not the motive.

supply chain musl openssh alpine xz threat modeling
← back to all posts