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
Published May 28, 2026Reviewed 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

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

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)

Prevention & Best Practices

This vulnerability is a textbook example of why C string handling deserves extra scrutiny. Here's how to prevent similar issues in your own code:

1. Prefer strlcpy and strncat Over strcpy and strcat

// Dangerous
strcpy(dest, src);

// Safer
strlcpy(dest, src, dest_size);

strlcpy (available on BSD and via libbsd on Linux) always null-terminates and never writes beyond dest_size bytes. It's not a perfect solution, but it prevents the worst outcomes.

2. Always Allocate strlen(s) + 1

This is a rule with no exceptions in C:

char *copy = malloc(strlen(original) + 1);  // +1 is mandatory

Consider writing a wrapper that enforces this:

char *safe_strdup(const char *s) {
    size_t len = strlen(s) + 1;
    char *copy = malloc(len);
    if (copy) memcpy(copy, s, len);
    return copy;
}

3. Test Your Math Utilities Independently

The next_power_of_2 bug could have been caught by a single unit test:

assert(next_power_of_2(1)  == 1);
assert(next_power_of_2(5)  == 8);
assert(next_power_of_2(16) == 16);
assert(next_power_of_2(17) == 32);

Utility functions that are used in security-sensitive contexts (memory allocation, bounds checking) deserve their own test suite.

4. Use AddressSanitizer (ASan) During Development

Compile with -fsanitize=address to catch heap overflows at runtime during testing:

gcc -fsanitize=address -g -o myapp myapp.c

ASan would have immediately flagged the off-by-one write in gravier_str_init the first time it was exercised.

5. Consider Safer Abstractions

If your project allows it, consider replacing manual string buffers with well-tested libraries:

  • C: SDS (Simple Dynamic Strings), used by Redis
  • C++: std::string with .append() and .reserve()
  • Rust: String and &str with compile-time memory safety guarantees

6. Relevant Security Standards


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.

Frequently Asked Questions

What is a buffer overflow vulnerability?

A buffer overflow occurs when a program writes data beyond the allocated memory boundary, potentially corrupting adjacent memory, crashing the application, or enabling arbitrary code execution.

How do you prevent buffer overflow in C?

Use bounds-checked functions like strncpy() or snprintf(), validate input lengths before copying, ensure correct size calculations, and consider using memory-safe alternatives or static analysis tools.

What CWE is buffer overflow?

Buffer overflow vulnerabilities are classified under CWE-120 (Buffer Copy without Checking Size of Input) and CWE-122 (Heap-based Buffer Overflow) for heap-specific cases.

Is using malloc() enough to prevent buffer overflow?

No, malloc() only allocates memory—it doesn't prevent overflows. You must ensure the allocated size is correct and that subsequent write operations don't exceed the buffer boundary.

Can static analysis detect buffer overflow?

Yes, static analysis tools can detect many buffer overflow patterns, especially those involving incorrect size calculations, missing bounds checks, and unsafe string functions like strcpy().

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #39

Related Articles

high

How insecure string copy functions happen in C apputils.c and how to fix it

A high-severity buffer overflow vulnerability was discovered in `src/apps/common/apputils.c`, where `strncpy()` was used without guaranteed null-termination across four call sites — including the `sock_bind_to_device()` and `getdomainname()` functions. The fix replaces all unsafe `strncpy()` calls with `snprintf()`, which enforces both length bounds and automatic null-termination. Left unpatched, these flaws could allow an attacker to corrupt memory, crash the process, or potentially execute arb

critical

How integer overflow in buffer size calculation happens in C++ and how to fix it

A critical integer overflow vulnerability was discovered in OpenCV's HAL filter implementation where multiplying image dimensions without overflow protection could allocate dangerously undersized buffers. An attacker supplying crafted image dimensions (e.g., 65536×65536) could trigger heap corruption through out-of-bounds writes. The fix promotes the calculation to 64-bit arithmetic with a single cast.

critical

How buffer overflow via strcpy() happens in C zlib and how to fix it

A critical buffer overflow vulnerability was discovered in `general/libzlib/gzlib.c` where multiple `strcpy()` and `strcat()` calls operated without bounds checking. An attacker controlling file paths or error messages could overflow destination buffers, potentially achieving arbitrary code execution. The fix replaces these unsafe string operations with bounded `memcpy()` calls that respect pre-calculated buffer lengths.

critical

How buffer overflow in rcdevice.c request parser happens in C and how to fix it

A critical buffer overflow vulnerability was discovered in `src/main/io/rcdevice.c` at line 489, where the RC device request parser wrote incoming data into a fixed-size buffer without validating against the hard-coded maximum capacity `RCDEVICE_PROTOCOL_MAX_DATA_SIZE`. An attacker controlling the device's I/O data stream could overflow the buffer by sending a payload longer than `expectedDataLength`, potentially achieving arbitrary code execution. The fix adds a second bounds check against the

critical

How buffer overflow via unchecked memcpy offset happens in C++ PCL point cloud parsing and how to fix it

A critical out-of-bounds read vulnerability was discovered in `pcpatch_pcl.cpp` where the `readFloat` lambda performed a `memcpy` operation using an untrusted offset value without validating buffer boundaries. An attacker could craft malicious PCD point cloud files with large offset values to read memory outside allocated buffers, potentially leaking sensitive data or causing crashes. The fix adds a bounds check ensuring `f->offset + sizeof(float)` stays within the row buffer before any memory c

critical

How buffer overflow in stb_image.h memcpy happens in C image parsing and how to fix it

A critical buffer overflow vulnerability was discovered in stb_image.h at line 4823, where a memcpy operation copied image data without validating buffer bounds. The multiplication of width (x) and channel count (img_n) could overflow or exceed allocated memory, allowing attackers to corrupt memory through malicious PNG files. The fix adds an explicit size_t cast to prevent integer overflow during the buffer size calculation.