Skip to content

[libc++] Buffer overflow in basic_filebuf when writing to the stream

Low
kbeyls published GHSA-p57v-c3wj-w37w Feb 3, 2026

Package

No package listed

Affected versions

< 21

Patched versions

21

Description

Summary

When writing to a file using std::fstream, a buffer overflow may happen where we will write characters outside of the buffer owned internally by the std::basic_filebuf. This happens in the std::basic_filebuf::overflow method (the name of the method has nothing to do with the buffer overflow being reported). The buffer overflow only happens if writing to the underlying file using std::fwrite fails, which mitigates the impact of the bug. For example, this can happen if the disk is full, or presumably if the permissions of the file are changed, or anything else that causes std::fwrite to fail.

By calling std::basic_filebuf::overflow successively, it should be possible to write arbitrary content past the end of the buffer being managed by the basic_filebuf.

Details

This reproduces with something like this (pasting since that's what I have in the test suite):

#include <cstddef>
#include <fstream>
#include <iostream>
#include <string>

#include "platform_support.h"

int main() {
  std::string temp = get_temp_file_name();

  std::basic_filebuf<wchar_t> fbuf;
  if (!fbuf.open(temp, std::ios::out | std::ios::trunc))
    return 1;

  std::basic_string<wchar_t> large_block(200000000, L'x');

  std::size_t bytes = 0;
  wchar_t ret;
  while ((ret = fbuf.sputn(large_block.data(), large_block.size())) != 0) {
    bytes += ret;
    if (bytes % 100000000 == 0) // print every 100mb
      std::cout << "bytes written = " << bytes << std::endl;
  }

  fbuf.close();
  return 0;
}

This program basically creates a temporary file and then writes to it until the disk is full. When the disk is full, the intent is for sputn to return 0 (i.e. it wrote 0 bytes), then close the file and exit. However, what happens instead is an out-of-bounds write.

To understand what happens, we need to understand that the C++ file streams possess two buffers they use to read/write: the get area for reading and the put area for writing. Each of these areas is described by three pointers: a pointer to the beginning of the area, a pointer to one-past-the-end of the area, and a pointer to the current position inside the area. In this bug, we are interested by the put area, which is represented by the following three pointers:

  • pbase(): beginning of the put area
  • pptr(): the current cursor within the put area
  • epptr(): one-past-the-end of the put area

These names are terrible but they are C++ Standard APIs. For mnemonics, pptr() stands for put pointer, and epptr() is end-put-pointer.

There is an important invariant that pbase() <= pptr() <= epptr(). That invariant must be kept or else we risk writing outside of the buffer we allocated. Furthermore, an important detail is that the buffer that underlies the put area is actually one element larger than [pbase, epptr). The reason for that will become clear below.

What normally happens in the sputn call above is that we'll fill up the put area successively. At some point, the put area will be full (meaning pptr() == epptr()). At that point, we will call overflow(c) where c is the next character we're trying to write to the stream. The goal of overflow is to effectively commit the contents of the put area to the file, including the overflowing character c. basic_filebuf::overflow does this by writing c at the location of pptr() and then calling std::fwrite with the contents of the whole put area plus the overflowing character c. Note that since pptr() == epptr() when we enter overflow(), we would be tempted to think that we're writing the overflow character c outside of the buffer — in reality, we plan for that by allocating one additional element after the end of the put area, as mentioned above. In other words, we're writing c one-past-the-end of the put area, but that's fine cause that's still memory that we own.

At that point, we have pptr() == epptr() + 1, and it should be noted that we're violating the class invariant. This state is supposed to be temporary until we finish the std::fwrite call, succeed, and then reset our put area to be empty (which makes sense cause we committed all of the put area to the file).

This is where our bug comes in. If the std::fwrite call fails, we return failure from the overflow function immediately, and we don't do anything special to restore our put area. That means that we return from the function with pptr() == epptr() + 1, which breaks our invariant. If we call overflow() again with that invariant broken, we'll notice that the put area is full, we'll write an overflow character to pptr() + 1 (which will now be two-past-the-end of the put area), and we'll try calling std::fwrite again. But at that point, pptr() + 1 is equal to epptr() + 2, and that's truly outside of the range that we allocated initially, meaning we're writing arbitrary bytes to a location outside what we allocated.

This can be performed over and over again to write as many bytes as desired to the memory location after the put area. However, it requires the std::fwrite call to fail, which may not be easy to set up. That's probably why we never noticed this issue: getting fwrite to fail is non-trivial.

rdar://152513681

PoC

See above.

Impact

I don't know how to exploit this bug. I'm not certain whether it's possible, but people are pretty creative. This requires fwrite to fail which makes this a lot more difficult to pull out than otherwise. Still, I think it's worth treating this bug with some amount of care.

Severity

Low

CVSS overall score

This score calculates overall vulnerability severity from 0 to 10 and is based on the Common Vulnerability Scoring System (CVSS).
/ 10

CVSS v3 base metrics

Attack vector
Network
Attack complexity
Low
Privileges required
None
User interaction
None
Scope
Unchanged
Confidentiality
None
Integrity
None
Availability
None

CVSS v3 base metrics

Attack vector: More severe the more the remote (logically and physically) an attacker can be in order to exploit the vulnerability.
Attack complexity: More severe for the least complex attacks.
Privileges required: More severe if no privileges are required.
User interaction: More severe when no user interaction is required.
Scope: More severe when a scope change occurs, e.g. one vulnerable component impacts resources in components beyond its security scope.
Confidentiality: More severe when loss of data confidentiality is highest, measuring the level of data access available to an unauthorized user.
Integrity: More severe when loss of data integrity is the highest, measuring the consequence of data modification possible by an unauthorized user.
Availability: More severe when the loss of impacted component availability is highest.
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:N

CVE ID

No known CVE

Weaknesses

No CWEs