Missing pending-buffer size cap in Strategy::rle triggers out-of-bounds write in copy_block
Version of Beast
BOOST_BEAST_VERSION 360 (boost/beast/version.hpp)
- commit
56a7c3af4901d0c63c7c1149d79bbf8856341343 (boost-1.91.0-16-g56a7c3af)
- Also reproducible with Boost 1.83 /
BOOST_BEAST_VERSION 347 (system package) and earlier releases — the missing check is long-standing.
Steps necessary to reproduce
- Install a recent Boost and clang, e.g. on Ubuntu:
sudo apt install libboost-dev clang
- Save the program below as
poc.cpp
- Build and run:
clang++ -fsanitize=address -g -O1 -std=c++17 poc.cpp -o poc && ./poc
#include <boost/beast/zlib/deflate_stream.hpp>
#include <boost/beast/zlib/zlib.hpp>
#include <cstdint>
#include <vector>
namespace zlib = boost::beast::zlib;
int main()
{
// Documented-valid configuration (deflate_stream.hpp:107-115):
// memLevel=1 -> internal pending buffer is 512 bytes
// level=0 forces a stored block
// Strategy::rle routes to f_rle, which (unlike f_stored) does not
// cap the stored-block size against pending_buf_size_.
zlib::deflate_stream ds;
ds.reset(0, 9, 1, zlib::Strategy::rle);
std::vector<std::uint8_t> input_data(508, 'A');
std::vector<std::uint8_t> compressed(
zlib::deflate_upper_bound(input_data.size()) + 64);
zlib::z_params zp;
zp.next_in = input_data.data();
zp.avail_in = input_data.size();
zp.next_out = compressed.data();
zp.avail_out = compressed.size();
boost::system::error_code ec;
ds.write(zp, zlib::Flush::finish, ec);
return 0;
}
Observed (ASan, clang 18.1.3, Boost 1.83 / Beast 347):
ERROR: AddressSanitizer: heap-buffer-overflow
WRITE of size 508
#1 deflate_stream::copy_block deflate_stream.ipp:1334
#2 deflate_stream::tr_stored_block deflate_stream.ipp:1393
#3 deflate_stream::tr_flush_block deflate_stream.ipp:1488
#4 deflate_stream::flush_block deflate_stream.ipp:1703
#5 deflate_stream::f_rle deflate_stream.ipp:2258
#6 deflate_stream::deflate_rle deflate_stream.hpp:683
#7 deflate_stream::doWrite deflate_stream.ipp:433
#8 deflate_stream::write deflate_stream.hpp:299
#9 main poc.cpp:27
The 508-byte write overruns the 512-byte pending buffer by 1 byte. The overrun is not limited to 1 byte: with a 60 KB input and windowBits = 15 (same config), the first flush produces WRITE of size 32509 (~32 KB past the 512-byte buffer); with memLevel = 2 it produces WRITE of size 60000 (~59 KB past the 1 KB buffer). So the primitive is a genuine heap out-of-bounds write whose size scales linearly with input length.
Expected behavior
The stream should either cap/split the stored block so it fits the pending buffer, or grow the buffer. With memLevel = 1 the pending buffer is 512 bytes (pending_buf_size_ = lit_bufsize_ * 4, lit_bufsize_ = 1 << (memLevel + 6)); a 508-byte stored block plus its 5-byte header needs 513 bytes, so copy_block writes 1 byte past the buffer. The overflow grows linearly with input length (observed 32 KB+ writes with larger inputs and windowBits = 15).
Security Impact
Genuine heap out-of-bounds write (CWE-787): the overflowed bytes are caller-controlled input, and the overrun grows with input size (from 1 byte up to ~32-59 KB per flush in our tests). Potential risk is corruption of adjacent heap data and denial of service under ASan; exploitation was not demonstrated. Practical exposure is limited: triggering requires the unusual documented-valid configuration (memLevel ≤ 2, level = 0, Strategy::rle), and no default integration path uses it.
Root cause (brief)
copy_block (deflate_stream.ipp:1319-1336) writes the stored-block body with std::memcpy(&pending_buf_[pending_], buf, len) and no check against pending_buf_size_. The f_rle finish path (deflate_stream.ipp:2255-2261) flushes the whole accumulated input as a single stored block (tr_flush_block forces a stored block when level == 0, deflate_stream.ipp:1473-1488), whereas the sibling f_stored path explicitly caps the block at pending_buf_size_ - 5 (deflate_stream.ipp:1863-1868). f_rle has no such cap.
All relevant compiler information
- clang version 18.1.3 (Ubuntu), flags:
-fsanitize=address -g -O1 -std=c++17 -fno-omit-frame-pointer
- libboost-dev 1.83 (
/usr/include/boost, BOOST_BEAST_VERSION 347)
- Also reproduced with the same flags on commit
56a7c3af (BOOST_BEAST_VERSION 360)
Submission Statement
This report was produced by FuzzAnything's AI-assisted library fuzzer and manually verified by a team member. We reviewed the PoC against the upstream API documentation — call order, parameters, and memory ownership — and found no API misuse.
Signed-off-by: FuzzAnything fuzzanything@gmail.com
Missing pending-buffer size cap in Strategy::rle triggers out-of-bounds write in copy_block
Version of Beast
BOOST_BEAST_VERSION360 (boost/beast/version.hpp)56a7c3af4901d0c63c7c1149d79bbf8856341343(boost-1.91.0-16-g56a7c3af)BOOST_BEAST_VERSION347 (system package) and earlier releases — the missing check is long-standing.Steps necessary to reproduce
sudo apt install libboost-dev clangpoc.cppclang++ -fsanitize=address -g -O1 -std=c++17 poc.cpp -o poc && ./pocObserved (ASan, clang 18.1.3, Boost 1.83 / Beast 347):
The 508-byte write overruns the 512-byte pending buffer by 1 byte. The overrun is not limited to 1 byte: with a 60 KB input and
windowBits = 15(same config), the first flush producesWRITE of size 32509(~32 KB past the 512-byte buffer); withmemLevel = 2it producesWRITE of size 60000(~59 KB past the 1 KB buffer). So the primitive is a genuine heap out-of-bounds write whose size scales linearly with input length.Expected behavior
The stream should either cap/split the stored block so it fits the pending buffer, or grow the buffer. With
memLevel = 1the pending buffer is 512 bytes (pending_buf_size_ = lit_bufsize_ * 4,lit_bufsize_ = 1 << (memLevel + 6)); a 508-byte stored block plus its 5-byte header needs 513 bytes, socopy_blockwrites 1 byte past the buffer. The overflow grows linearly with input length (observed 32 KB+ writes with larger inputs andwindowBits = 15).Security Impact
Genuine heap out-of-bounds write (CWE-787): the overflowed bytes are caller-controlled input, and the overrun grows with input size (from 1 byte up to ~32-59 KB per flush in our tests). Potential risk is corruption of adjacent heap data and denial of service under ASan; exploitation was not demonstrated. Practical exposure is limited: triggering requires the unusual documented-valid configuration (
memLevel ≤ 2,level = 0,Strategy::rle), and no default integration path uses it.Root cause (brief)
copy_block(deflate_stream.ipp:1319-1336) writes the stored-block body withstd::memcpy(&pending_buf_[pending_], buf, len)and no check againstpending_buf_size_. Thef_rlefinish path (deflate_stream.ipp:2255-2261) flushes the whole accumulated input as a single stored block (tr_flush_blockforces a stored block whenlevel == 0,deflate_stream.ipp:1473-1488), whereas the siblingf_storedpath explicitly caps the block atpending_buf_size_ - 5(deflate_stream.ipp:1863-1868).f_rlehas no such cap.All relevant compiler information
-fsanitize=address -g -O1 -std=c++17 -fno-omit-frame-pointer/usr/include/boost,BOOST_BEAST_VERSION347)56a7c3af(BOOST_BEAST_VERSION360)Submission Statement
This report was produced by FuzzAnything's AI-assisted library fuzzer and manually verified by a team member. We reviewed the PoC against the upstream API documentation — call order, parameters, and memory ownership — and found no API misuse.