Back to Blog
critical SEVERITY9 min read

Critical Buffer Overflow in gravier-str.h: How Broken Bit Shifts and Missing Bounds Checks Created a Memory Corruption Vulnerability

A critical buffer overflow vulnerability was discovered in `gravier/gravier-str.h`, where a subtle typo in the `next_power_of_2()` function combined with off-by-one allocation errors and unchecked `strcpy()` calls created a dangerous memory corruption pathway. Because this code was directly reachable from user-facing menu text input and the s7 scripting engine, an attacker could potentially exploit it to crash the application or execute arbitrary code. The fix corrects the broken bit-shift opera

O
By Orbis AppSec
•Technically reviewed by Anupam Mediratta•Published May 28, 2026•Reviewed June 3, 2026

Answer Summary

This is a critical buffer overflow vulnerability (CWE-122) in C's `gravier-str.h` caused by a broken bit-shift operation in `next_power_of_2()` that calculated incorrect buffer sizes, combined with unchecked `strcpy()` calls. The fix corrects the bit-shift logic to properly calculate power-of-2 sizes and adds bounds checking before string copy operations to prevent heap buffer overflows.

Vulnerability at a Glance

cweCWE-122
fixCorrected bit-shift operator and added bounds checking before strcpy()
riskRemote code execution or application crash via user input
languageC
root causeBroken bit-shift in next_power_of_2() caused undersized buffer allocation
vulnerabilityHeap Buffer Overflow

Introduction

Buffer overflows are among the oldest and most dangerous vulnerability classes in software security. Despite decades of awareness, they continue to appear in modern codebases — often hiding in utility code that developers trust implicitly. This post examines a critical-severity buffer overflow discovered in gravier/gravier-str.h, a string management utility that underpins user-facing input handling and scripting engine integration.

What makes this vulnerability particularly instructive is that it wasn't caused by a single mistake. It was the result of three compounding bugs — a typo that silently broke a core math function, an allocation that forgot to account for the null terminator, and reallocation logic that checked the wrong condition. Together, they created a reliable path to heap corruption.

If you write C or C++, or maintain any code that performs manual string management, this is a vulnerability worth understanding deeply.


The Vulnerability Explained

Background: What is gravier_str?

gravier_str is a dynamic string type — a common pattern in C codebases that lack a built-in string class. It wraps a char * pointer alongside a length field, and provides functions like gravier_str_init, gravier_str_append, and gravier_str_replace to manage the string's lifetime and growth.

The design relies on a helper function, next_power_of_2(), to calculate buffer sizes. The idea is simple: always allocate a power-of-two number of bytes so that reallocations are infrequent and predictable.

Unfortunately, this helper function was silently broken.


Bug #1: The Broken Bit-Shift Typo

Here is the original next_power_of_2() implementation:

// VULNERABLE CODE
static inline int32_t next_power_of_2(int32_t i)
{
    --i;
    i |= 1 >> 1;   // ← Bug: shifts the literal 1, not i
    i |= 1 >> 2;   // ← Bug
    i |= 1 >> 4;   // ← Bug
    i |= 1 >> 8;   // ← Bug
    i |= 1 >> 16;  // ← Bug
    ++i;
    return i;
}

The classic bit-spreading algorithm for finding the next power of two works by OR-ing a value with right-shifted copies of itself. Each shift propagates the highest set bit downward, filling all lower bits with ones — so that ++i produces the next power of two.

The bug here is a single-character typo: 1 >> N instead of i >> N. Because integer literal 1 is being shifted rather than i, the result is always 0 (right-shifting 1 by 1 or more positions yields 0 in integer arithmetic). This means the function effectively becomes:

--i;
i |= 0;  // no-op, five times
++i;
return i;  // returns the original value unchanged

The function no longer computes the next power of two — it just returns its input. For most inputs, this produces an undersized buffer. For example, if gstr->len is 10, the function should return 16, but instead returns 10 — and the allocation is too small.


Bug #2: Off-by-One in the Initial Allocation

Even if next_power_of_2() had worked correctly, there was a second problem in gravier_str_init:

// VULNERABLE CODE
gstr->len = strlen(base);
gstr->str = malloc(next_power_of_2(gstr->len));  // ← no +1 for null terminator
strcpy(gstr->str, base);

strlen() returns the number of characters in the string, not including the null terminator (\0). When you allocate malloc(strlen(base)) bytes and then strcpy into it, you write one byte past the end of the allocated buffer — a classic off-by-one heap overflow.

For a 7-character string like "gravier", strlen returns 7. You allocate 7 bytes (or, with the broken power-of-two function, possibly even fewer). strcpy writes 8 bytes (7 characters + null terminator), overwriting one byte of adjacent heap memory.


Bug #3: Incorrect Reallocation Guard

The append functions also contained flawed growth logic:

// VULNERABLE CODE
int gravier_str_append(struct gravier_str *gstr, char *str) {
    int other_len = strlen(str);
    int next_big = next_power_of_2(gstr->len);  // ← doesn't account for other_len

    if (gstr->len + other_len >= next_big) {     // ← checks wrong threshold
        char *tmp = malloc(next_big);
        // ...
    }
    strcpy(gstr->str + gstr->len, str);          // ← writes without bounds check
}

There are two problems here:

  1. next_big is calculated from gstr->len alone, ignoring the length of the string being appended. So even when a reallocation is triggered, the new buffer may still be too small to hold the combined string.

  2. The condition gstr->len + other_len >= next_big may not trigger when it should, because next_big (being computed from only gstr->len) can be larger than the actual current buffer size, masking the overflow condition.

The strcpy at the end then writes into a buffer that hasn't been grown sufficiently — a reliable heap write-out-of-bounds.


Real-World Impact

This vulnerability is directly reachable from:

  • User-facing menu text input — any string typed by the user can flow into these functions
  • The s7 scripting engine — scripts can construct and manipulate strings programmatically

An attacker who can supply crafted input (e.g., a long menu label or a script that builds long strings) can:

  • Corrupt adjacent heap allocations, leading to application crashes
  • Overwrite heap metadata, potentially leading to arbitrary code execution
  • Bypass ASLR in some heap exploitation scenarios by leveraging predictable allocation patterns

This maps to CWE-122: Heap-based Buffer Overflow and is rated Critical (CVSS 9.x range) when user-controlled input is involved.


The Fix

The patch addresses all three bugs with targeted, minimal changes.

Fix #1: Correct the Bit-Shift Operations

// FIXED CODE
static inline int32_t next_power_of_2(int32_t i)
{
    --i;
    i |= i >> 1;   // ← now correctly shifts i
    i |= i >> 2;
    i |= i >> 4;
    i |= i >> 8;
    i |= i >> 16;
    ++i;
    return i;
}

Changing 1 >> N to i >> N restores the intended bit-spreading behavior. Now the function correctly returns the smallest power of two that is greater than or equal to its input. For example:
- Input 10 → returns 16
- Input 16 → returns 16
- Input 17 → returns 32

This ensures all allocations are at least as large as needed, with room to spare.


Fix #2: Account for the Null Terminator

// FIXED CODE
gstr->len = strlen(base);
gstr->str = malloc(next_power_of_2(gstr->len + 1));  // ← +1 for '\0'
strcpy(gstr->str, base);

Adding + 1 before passing to next_power_of_2 ensures the allocated buffer always has room for the null terminator. This is the single most common off-by-one mistake in C string handling, and it's now correctly addressed.


Fix #3: Correct the Reallocation Logic

// FIXED CODE
int gravier_str_append(struct gravier_str *gstr, char *str) {
    int other_len = strlen(str);
    int next_big = next_power_of_2(gstr->len + other_len + 1);  // ← full combined size

    if (next_big > next_power_of_2(gstr->len + 1)) {  // ← compares actual buffer sizes
        char *tmp = malloc(next_big);
        // ...
    }
    strcpy(gstr->str + gstr->len, str);
}

The same pattern is applied consistently to gravier_str_append_int and gravier_str_append_float:

// FIXED: gravier_str_append_int
int other_len = 64;
int next_big = next_power_of_2(gstr->len + other_len + 1);

if (next_big > next_power_of_2(gstr->len + 1)) {
    // reallocate
}

// FIXED: gravier_str_append_float
int other_len = 128;
int next_big = next_power_of_2(gstr->len + other_len + 1);

if (next_big > next_power_of_2(gstr->len + 1)) {
    // reallocate
}

Now next_big represents the buffer size needed for the combined string (current content + new content + null terminator). The condition compares this against the size of the current buffer (computed as next_power_of_2(gstr->len + 1)). If the required size exceeds the current allocation, a reallocation occurs — and the new buffer is guaranteed to be large enough.


Before vs. After Summary

Location Before After
next_power_of_2 1 >> N (always 0) i >> N (correct)
gravier_str_init malloc(next_power_of_2(len)) malloc(next_power_of_2(len + 1))
gravier_str_append next_big = next_power_of_2(gstr->len) next_big = next_power_of_2(gstr->len + other_len + 1)
Realloc condition gstr->len + other_len >= next_big next_big > next_power_of_2(gstr->len + 1)

Conclusion

This vulnerability is a reminder that critical security bugs often hide in the most mundane utility code. A one-character typo (1 >> instead of i >>) silently disabled a buffer-sizing function, and two additional allocation mistakes compounded the damage into a reliable, user-reachable heap overflow.

The key takeaways for developers:

  • Test your math. Functions that compute sizes or offsets should have unit tests with boundary values.
  • Always include the null terminator in C string allocations. No exceptions.
  • Reallocation logic must account for the full combined size, not just the current size.
  • Use sanitizers. ASan and Valgrind exist precisely to catch these bugs before they reach production.
  • Treat user-reachable string paths as attack surfaces. Any code that processes external input deserves extra scrutiny.

The fix here was small — a handful of lines — but the impact of leaving it unfixed could have been severe. Automated security scanning caught what code review missed, and the patch was verified with both a build check and a re-scan. That's the security development lifecycle working as intended.


This vulnerability was identified and fixed by OrbisAI Security. Automated scanning, AI-assisted code review, and developer education work best together.

Prevention and further reading

Related Articles

critical

Aardvark.Cef.Process.Core Shared Memory Handler: Unvalidated memcpy

The shared memory handler in Aardvark.Cef.Process.Core failed to validate that the `length` parameter passed from JavaScript did not exceed the destination buffer size before calling `memcpy`. This allowed a compromised Chromium renderer process to corrupt heap memory by supplying an oversized `handle->length` value to the `openMapping` function. The fix adds explicit bounds checking using `GetArrayBufferByteLength()` before the copy operation.

high

XShmGetImage Heap Corruption: Unvalidated Image Height in streamproxy

The `XShmGetImage` function in the X11 shared memory image path copies pixel data row-by-row using `memcpy` without validating that the source image height matches the destination buffer height. An attacker or compromised server could provide an oversized source image, causing writes beyond the allocated heap buffer and triggering heap corruption or code execution.

critical

SGX Enclave ecall_store_data memcpy Buffer Overflow in 256-Byte

The Intel SGX enclave's trusted bridge functions `ecall_store_data` and `ecall_retrieve_data` used `memcpy()` to move data into and out of a fixed 256-byte `secure_storage` buffer without validating that `data_len` fit within destination boundaries. An attacker providing oversized `data_len` values could corrupt enclave memory, breaking SGX's confidentiality guarantees.

critical

How Buffer Overflow via strcpy() Happens in C++ XML Parsers and How to Fix It

A critical buffer overflow vulnerability was discovered in `buildroot-external/package/libxmlparser/xmlParser.cpp`, where the `toXMLString` function used `_tcscpy()` to write XML escape sequences into a destination buffer without any bounds checking. An attacker supplying a crafted XML document could overflow the buffer and potentially execute arbitrary code. The fix replaces all five unsafe `_tcscpy()` calls with `memcpy()` calls that copy only the exact number of bytes required for each escape

critical

How Heap Buffer Overflows Happen in C++ ZIP Extraction and How to Fix Them

A critical heap buffer overflow vulnerability was discovered in `TKLiveSync/unzip.cpp`, where ZIP archive entry names were copied into a `PATH_MAX`-sized heap buffer using `strcpy()` without any length validation. Since the ZIP specification allows entry names up to 65,535 bytes — far exceeding typical `PATH_MAX` values of 1,024 to 4,096 bytes — a crafted archive could overflow the buffer and corrupt heap memory. The fix replaces the unsafe `strcpy`/`dirname` pattern with `std::string` operation

medium

How Integer Overflow happens in C++ image processing and how to fix it

A signed integer overflow in OpenCV's `bilateralFilter.cpp` allowed the buffer size calculation `cal_width * cal_height * cn` to wrap around to a small or negative value, causing `padding.resize()` to allocate far less memory than needed. Subsequent `memcpy` operations would then write beyond the allocated buffer, creating a heap corruption primitive. The fix is a single targeted cast to `size_t` that promotes the multiplication to unsigned 64-bit arithmetic before any overflow can occur.