minecraft chunk-batch backpressure bypass (mc-308703)

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

on this page