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 calculations.c and how to fix it

A high-severity buffer overflow vulnerability was discovered in `src/calculations.c` at line 37, where a two-step `strncpy` + manual null-termination pattern left the door open for subtle memory safety bugs when copying string data into the `entry->type` field. The fix replaces both lines with a single `snprintf` call that handles bounds and null-termination atomically, eliminating the risk entirely. This is a common C pitfall that affects production CLI tools and can be exploited when attacker-

high

How integer truncation heap overflow happens in C++ UEFI ACPI parsing and how to fix it

A high-severity integer truncation vulnerability was discovered in `Mobility.Uefi.Acpi.cpp` where heap allocation sizes were stored in a 16-bit integer (`MO_UINT16`), causing silent truncation when the computed size exceeded 65535 bytes. This led to undersized heap allocations followed by out-of-bounds writes, exploitable by an attacker who can influence ACPI SRAT table contents in virtualized environments. The fix promotes the size variable to `MO_UINTN` (platform-native width) to prevent trunc

critical

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

A critical integer overflow vulnerability was discovered in the `nsh_setvar()` function in `nshlib/nsh_vars.c`, where the buffer size calculation `newsize = pstate->varsz + varlen` could wrap around, causing a heap buffer overflow. The fix adds overflow checking before the addition, preventing attackers with shell access from corrupting memory by setting variables with crafted names and values.

high

How buffer overflow via sprintf() happens in C networking code and how to fix it

A high-severity buffer overflow vulnerability was discovered in `profile.c` where `sprintf()` was used to format server addresses without any bounds checking. An attacker who could influence the `SERVER_BASE_PORT` value or trigger integer overflow in the port calculation could write beyond the `server_address` buffer. The fix replaces `sprintf()` with `snprintf()` using explicit buffer size limits at both call sites (lines 99 and 220).

critical

How integer overflow happens in C reliable.c and how to fix it

A critical integer overflow vulnerability was discovered in `reliable.c` at line 1299, where the `packet_buffer_size` calculation used signed `int` arithmetic that could wrap to a negative or undersized value when large `fragment_size` values were involved. By casting each operand to `size_t` before multiplication, the fix eliminates the overflow risk entirely and ensures the allocated buffer is always large enough to hold the reassembled packet data.

high

How insecure string copy functions happen in C (cyw43.c) and how to fix it

Three unsafe string copy calls in `src/cyw43.c` — including a bare `strcpy()` and two `strncpy()` calls — created buffer overflow risks in a CYW43 Wi-Fi driver emulation layer. The fix replaces all three with `snprintf()`, which enforces buffer size limits and guarantees null-termination in a single, consistent operation. Left unaddressed, these vulnerabilities could allow an attacker controlling input like a TAP interface name or SSID to corrupt adjacent memory and potentially execute arbitrary