Skip to content

Missing pending-buffer size cap in Strategy::rle triggers out-of-bounds write in copy_block #3109

Description

@little-daimi

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

  1. Install a recent Boost and clang, e.g. on Ubuntu:
    sudo apt install libboost-dev clang
  2. Save the program below as poc.cpp
  3. 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

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions