minecraft chunk-batch backpressure bypass (mc-308703)
on this page
minecraft: java edition streams terrain to a client in batches and waits for the client to confirm each batch before sending the next. the confirmation is tracked by one counter on the server. from 1.20.2 through 26.1.2 that counter had no floor, so a client that confirmed batches it never received could drive it negative and hold the server’s send-gate open forever. a single joined player could then force the server to push chunk data with no flow control, growing its off-heap network buffers without bound and stalling the main thread for everyone on the server.
mojang fixed it in 26.2 with a one-line clamp. there is no cve, no advisory, and no changelog entry, so this page is the only public record. i reported it privately as mojira MC-308703 on 2026-06-09.
chunk batching, briefly
chunk batching arrived in 1.20.2 as server-side flow control. rather than sending every
visible chunk at once, PlayerChunkSender sends a batch, increments unacknowledgedBatches,
and keeps sending only while unacknowledgedBatches < maxUnacknowledgedBatches. the client
sends ServerboundChunkBatchReceived once it has processed a batch; the server decrements the
counter and sends more. the intent is that a slow client never gets flooded, because the
sender paces itself to whatever rate the receiver acknowledges.
the flaw is that the pacing trusts the client to tell the truth about what it received.
the bug
the decrement had no lower bound. onChunkBatchReceivedByClient did the equivalent of
unacknowledgedBatches-- on every inbound ServerboundChunkBatchReceived, and nothing checked
whether a batch was actually outstanding. the client decides how many acknowledgements to send.
send more than the server has batches in flight and the counter goes negative and stays there,
so the unacknowledgedBatches < maxUnacknowledgedBatches test in sendNextChunks is always
true. the send-gate never closes.
// vulnerable: 1.20.2 through 26.1.2
this.unacknowledgedBatches--; // no floor; goes negative on spurious acks impact
with the gate stuck open, the server sends chunk data as fast as sendNextChunks runs,
ignoring whether the client is draining it. the encoded chunk bytes queue in netty’s outbound
buffers. those buffers are off-heap, so they grow past -Xmx without tripping a java heap
error, and the process footprint climbs until the host kills it. the main server thread also
spends its per-tick budget generating and encoding chunks for a client that is not consuming
them, so tick time rises and the server stalls for every connected player, not only the
attacker.
reaching the code needs a joined session and nothing else: no permissions, no operator status,
no client modification beyond sending a stock packet more often than the protocol expects. i
score it 6.5, CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:N/A:H: remote, low-privilege,
availability-only. paper and spigot already blunt it with player-max-chunk-send-rate, which
caps outbound chunk rate no matter what the counter says; vanilla and fabric shipped no such
cap.
reproduction
i reproduced it live against a stock 26.1.2 server and a fabric server, driving one joined
connection that floods ServerboundChunkBatchReceived. a controlled a/b run isolates the
counter from ordinary connection load: same client, one run sending one acknowledgement per
batch, one run flooding acknowledgements. the paced run holds steady; the flooding run shows
monotonic off-heap growth and rising tick time. the vulnerable path is the batching code added
in 1.20.2, so 1.20.1 and earlier are unaffected.
the fix
26.2 (protocol 776), shipped 2026-06-16, floors the counter. the fix i reported was
unacknowledgedBatches = Math.max(0, unacknowledgedBatches - 1). the shipped jar does the same
thing longhand and adds a NaN guard on desiredChunksPerTick:
this.unacknowledgedBatches--;
if (this.unacknowledgedBatches < 0) {
this.unacknowledgedBatches = 0;
} a negative counter can no longer bank credit against the send-gate. affected range: 1.20.2 through 26.1.2; fixed in 26.2 and later.
disclosure timeline
- 2026-06-09: reported to mojang as mojira MC-308703, a private security report.
- 2026-06-16: fixed in the 26.2 release.
- 2026-06-21: fix confirmed at source in the shipped 26.2 server jar.
mojang does not assign cves for server-side security fixes and does not describe them in patch notes, so the ticket stays private and the 26.2 changelog is silent on it. that is the normal outcome for this class of report, and the reason i wrote it up here.
references
- minecraft java edition 26.2 (minecraft wiki)
- minecraft java edition 26.2 patch notes
- java edition protocol reference (minecraft wiki)
- mojira MC-308703 (private security report; not publicly viewable)