Skip to content

DeComp(): make the grow step actually double + guard empty input - #500

Draft
IrPgFKS0 wants to merge 1 commit into
BeamMP:minorfrom
IrPgFKS0:fix/decomp-grow-actually-doubles
Draft

DeComp(): make the grow step actually double + guard empty input#500
IrPgFKS0 wants to merge 1 commit into
BeamMP:minorfrom
IrPgFKS0:fix/decomp-grow-actually-doubles

Conversation

@IrPgFKS0

@IrPgFKS0 IrPgFKS0 commented Jul 5, 2026

Copy link
Copy Markdown

Problem

The grow step in DeComp() (src/Common.cpp) doesn't do what its comment says:

// if decompression fails, we double the buffer size (up to the allowed limit) and try again
output_buffer.resize(std::max<size_t>(output_buffer.size() * 2, MAX_DECOMPRESSION_BUFFER_SIZE));

std::max(size * 2, 30MB) is almost always 30 MB, so every packet that needs one grow allocates the full 30 MB instead of doubling — on every such packet, on the hot receive path. The intended capped doubling is std::min.

Fix

  • Change the grow to std::min<size_t>(output_buffer.size() * 2, MAX_DECOMPRESSION_BUFFER_SIZE) so it actually doubles up to the cap (the existing >= cap check above still terminates the loop).
  • Add an empty-input guard (throws, same error path as other invalid inputs). This is required for the min-fix to be safe: with std::min, a zero-length body (e.g. a bare 4-byte ABG: frame from a buggy/malicious peer) would keep the buffer at 0 forever (0 * 2 stays 0) and spin the loop — today's std::max is accidentally the only thing preventing that. The guard also skips a pointless 30 MB allocation + two uncompress calls for input that can never decompress.

The two changes are intentionally in one commit because each alone changes the safety story: min without the guard introduces an infinite loop; the guard without min leaves the 30 MB-per-grow over-allocation.

How this was found

Found while auditing decompression robustness on a LAN fork of BeamMP (the launcher's DeComp has the unguarded-doubling variant of this bug, submitted separately as BeamMP-Launcher#257); the equivalent fix has been running in that fork's server for weeks of sessions.

Transparency

This fix comes from an AI-assisted fork: the bug was found and the patch written with the help of an AI coding tool (Claude), then tested by a human in real multiplayer sessions. Given this project's policy on AI-generated code, it's submitted as a draft for the maintainers to decide — happy to close it if that's not wanted, or for a maintainer to re-implement it independently.

🤖 Generated with Claude Code

The grow used std::max(size * 2, 30MB), which is almost always 30MB --
every packet that needed one grow allocated the full cap instead of
doubling as the adjacent comment intends. Use std::min so it doubles up
to the cap (the existing >= cap check still terminates the loop).

Guard empty input up front (throw, same error path as other invalid
input): required for the capped doubling to be safe -- with std::min a
zero-length body (e.g. a bare 4-byte "ABG:" frame) would keep the
buffer at 0 forever and spin the loop; it also skips a pointless 30MB
allocation + two uncompress calls for input that can never decompress.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant