Security · Analysis
The Code Was Public for a Month. The Reason Wasn’t
Polygon fixed denial-of-service flaws in both of its clients, shipped them through two hard forks, and said so on August 29. The hardening itself had been readable in a public repository since July 27 — switched off, and unexplained.
On August 29, Polygon Labs' Validators Support Team posted a security releases review to the project forum, describing several previously undisclosed vulnerabilities in the two clients that run Polygon PoS. None had been exploited on mainnet, the team said. All of them were already fixed, because the fixes had shipped through two hard forks that had already activated.
That is coordinated disclosure done in the correct order, and it deserves to be said plainly before anything else: patch first, activate, then tell people. The chains that have ended up on this desk's front page for the opposite sequence — Cosmos EVM most recently — lost real money doing it backwards.
But the public record of how Polygon got there is more interesting than the summary, and it complicates the tidy version.
What was actually broken
The flaws split across Polygon's two clients. Bor is the execution layer, the Ethereum-derived client that produces blocks. Heimdall is the consensus and checkpointing layer, built on a Polygon fork of the Cosmos SDK.
The Austin hard fork, carried by Bor v2.10.0, addressed two denial-of-service problems. In the first, a block carrying enough state-sync events could, per the forum post, "make block processing take long enough to transiently stall the chain"; the fix imposes a per-block cap on state-sync gas consumption. The second is cruder and more interesting: an unbounded field on the wire called TxDependency, which a block producer could stuff with an arbitrarily large blob of data. Any peer that tried to process the block would crash. The fix removes the field.
The Kyoto hard fork, carried by Heimdall v0.11.0, is a longer list. Cointelegraph, which reported the disclosure first, characterised the most severe issue as one where a specially crafted transaction could force validators into excessive processing work. Reading the merged pull request behind it, #628, gives the mechanism: a bound on the nesting depth of google.protobuf.Any messages, enforced by a cheap byte-level pre-scan at both mempool admission and on the consensus path. A deeply nested message is expensive to decode. Enforcing the bound at only one of those two points would have let a transaction that the mempool rejected still arrive inside a proposed block.
A second Kyoto change, from #635, caps the number of coins a transaction may declare in its fee — placed, the PR notes, ahead of the validation step whose own scan over that list is linear in its length. Uncapped, that is quadratic work bought for the price of one transaction. The rest of the fork is consensus hardening: checkpoint-window continuity verified before the Bor RPC during vote-extension validation, milestone range support bound to the required parent hash, an idempotent producer-downtime handler, and a fix making the L1-event replay key injective for out-of-range log indexes.
The thirty-three days
Here is the part the forum post does not walk through. Pull request #628 — the one containing the nesting bound and most of the consensus hardening, with a description that explains each change and its purpose in detail — was merged into the public heimdall-v2 repository on July 27. It was gated behind a new hardfork flag, helper.IsKyoto, and that gate had no activation heights. The PR that supplied them, #635, landed on August 18 and says so directly: the heights "were left as TODO when the gating scaffolding first landed in an earlier PR."
What was visible, and when
| Date | Event | Public? |
|---|---|---|
| Jul 27 | Heimdall PR #628 merged — hardening described in full, gate inactive | Yes |
| Aug 13 | Bor v2.10.0 released; changelog is one line, no security wording | Yes |
| by Aug 18 | Kyoto active on Amoy and mainnet via a private fork | No |
| Aug 18 | Heimdall v0.11.0 released; PR #635 backfills the activation heights | Yes |
| Aug 29 | Forum post names the vulnerabilities | Yes |
So for the stretch between July 27 and mainnet activation, a description of what Polygon considered worth hardening sat in a public repository with the mitigation switched off. Polygon's answer to that is in #635's own text, and it is a good one: the real rollout never used the public repository. "Validated against the private security-release fork (heimdall-v2-private) that ran this exact behavior through Amoy and mainnet activation," the PR says. It describes itself as closing "the public disclosure step of an already-completed security rollout (private-fork release, testnet-first validation, mainnet activation all preceded this PR)."
That is a more disciplined process than most chains run. It is also not the same as the public commit being harmless. The private fork protected the validators who ran it; it did nothing about the fact that the hardening was legible on GitHub to anyone who went looking, weeks before the public gate was armed. The window closes at some point on or before August 18, since #635 states both networks had already activated by then — but the public record does not fix the mainnet activation date, and neither the forum post nor the release notes supply it. That is the one number that would settle how wide the gap actually was, and it is the number nobody published.
The changelog said nothing
Worth noting for anyone who upgrades by reading release notes: they would have learned nothing here. Bor v2.10.0's entire "What's Changed" section is a single line pointing at a release pull request. Heimdall v0.11.0 lists six entries, and the word "security" appears in none of them — the relevant ones read "consensus hardening behind Kyoto + tx-decode nesting bound" and "gate fee-coins cap decorator behind Kyoto." Accurate, and unreadable as a warning.
Both upgrades are mandatory and already active — Austin at mainnet block 91,949,700, Kyoto at Heimdall height 51,533,000. The forum post is direct about the consequence of ignoring them: nodes on earlier versions past the activation heights "have already fallen out of consensus." No state migration, genesis change or resync is required; it is a binary upgrade. Operators who missed the window are not at risk from these bugs. They are simply not on the network.
The Take
Polygon did this the right way round and the outcome shows it: forks activated, nothing exploited, disclosure afterward. The desk has spent a fortnight covering teams that inverted those steps and paid for it. The lesson worth carrying is narrower. A hardfork gate is a genuinely clever piece of tradecraft — ship the code cold, arm it later — but it only buys secrecy about timing, never about content. Pull request #628 explained its own reasoning carefully and well, which is exactly what good engineering hygiene demands and exactly what an attacker reads. If the private fork is doing the real security work, the public commit is not a fix in progress; it is an advisory with the severity field left blank. Polygon got away with a month of that, and the honest reason is that nobody was looking rather than that nothing was there. The fix for the next one costs nothing: publish the activation height alongside the disclosure, so the people auditing the process can measure the window instead of guessing at it.