Introduction
In the cordova-plugins/moonshine-stt repository — a speech-to-text plugin for Cordova — we discovered a high-severity use-after-free vulnerability in ggml-alloc.c at line 885. The ggml_gallocr_reserve_n_impl function, responsible for reserving memory for computation graph nodes, freed galloc->leaf_allocs and then immediately referenced the freed pointer in a sizeof expression on the very next line.
While sizeof on a dereferenced pointer is typically evaluated at compile time in most implementations, the C standard does not guarantee this for all cases, and static analysis correctly flags this as a use-after-free pattern. More critically, this creates an exploit primitive — a code pattern that automated attack tooling can chain with other weaknesses to achieve memory corruption or code execution in a speech processing pipeline that handles untrusted audio input.
The Vulnerability Explained
The Problematic Code
Here's the vulnerable code in ggml_gallocr_reserve_n_impl around line 882:
if (galloc->n_leafs < graph->n_leafs) {
free(galloc->leaf_allocs);
galloc->leaf_allocs = calloc(graph->n_leafs, sizeof(galloc->leaf_allocs[0]));
GGML_ASSERT(galloc->leaf_allocs != NULL);
}
The problem is subtle but dangerous:
- Line 883:
free(galloc->leaf_allocs)— the memory is deallocated - Line 884:
sizeof(galloc->leaf_allocs[0])— the freed pointer is dereferenced
After free() is called, galloc->leaf_allocs becomes a dangling pointer. The expression galloc->leaf_allocs[0] dereferences this dangling pointer. While many compilers resolve sizeof on a known type at compile time, the C standard considers any access through a freed pointer as undefined behavior. This means:
- The compiler is free to optimize this code in unexpected ways
- On some platforms or with certain compiler flags, this could actually read freed memory
- Future compiler optimizations could change the behavior silently
How Could This Be Exploited?
In this specific context, ggml-alloc.c is part of the GGML tensor library used in the Moonshine speech-to-text engine. Consider this attack scenario:
- An attacker crafts a malicious audio input that causes the computation graph to be resized (triggering
graph->n_leafs > galloc->n_leafs) - Between the
free()and thecalloc(), if the freed memory is reclaimed (e.g., by another thread or an allocator that coalesces immediately), thesizeofexpression could evaluate to an attacker-controlled value on implementations where it's not resolved at compile time - This could lead to an undersized allocation, followed by out-of-bounds writes when the
leaf_allocsarray is populated
Additionally, there was a secondary issue on line 837 where malloc was used instead of calloc for hash_values, leaving the allocated memory uninitialized — a potential information leak vector.
Real-World Impact
This code runs on Android devices as part of a Cordova plugin processing speech input. A malicious audio file or crafted input could potentially:
- Crash the application (denial of service)
- Corrupt memory used by the tensor computation engine
- In a worst case, achieve code execution on the user's device
The Fix
The pull request makes two targeted changes:
Change 1: Zero-initialized hash values (Line 837)
Before:
free(galloc->hash_values);
galloc->hash_values = malloc(sizeof(struct hash_node) * galloc->hash_set.size);
GGML_ASSERT(galloc->hash_values != NULL);
After:
free(galloc->hash_values);
galloc->hash_values = calloc(galloc->hash_set.size, sizeof(struct hash_node));
GGML_ASSERT(galloc->hash_values != NULL);
This change replaces malloc with calloc, ensuring the hash_values array is zero-initialized. This prevents information leakage from uninitialized memory and eliminates a class of bugs where stale hash values could cause incorrect graph allocation behavior.
Change 2: Eliminate use-after-free in leaf_allocs (Line 884-885)
Before:
free(galloc->leaf_allocs);
galloc->leaf_allocs = calloc(graph->n_leafs, sizeof(galloc->leaf_allocs[0]));
GGML_ASSERT(galloc->leaf_allocs != NULL);
After:
free(galloc->leaf_allocs);
galloc->leaf_allocs = NULL;
galloc->leaf_allocs = calloc(graph->n_leafs, sizeof(struct leaf_alloc));
GGML_ASSERT(galloc->leaf_allocs != NULL);
This fix addresses the vulnerability in two ways:
-
galloc->leaf_allocs = NULL— Immediately nullifies the dangling pointer afterfree(). If any code path accidentally accesses it before reallocation, it will trigger a clean NULL pointer dereference (detectable crash) rather than silent undefined behavior. -
sizeof(struct leaf_alloc)instead ofsizeof(galloc->leaf_allocs[0])— Eliminates the dereference of the freed pointer entirely by using the type name directly. This is semantically identical but avoids any reference to the freed memory.
Prevention & Best Practices
Defensive Coding Patterns for C Memory Management
-
Always NULL after free: Adopt the pattern of setting pointers to NULL immediately after freeing them:
c free(ptr); ptr = NULL; -
Use type names in sizeof, not pointer dereferences: Prefer
sizeof(struct type)oversizeof(*ptr)when the pointer might be in an invalid state. -
Prefer calloc over malloc:
calloczero-initializes memory, preventing information leaks and making bugs more deterministic. It also has overflow checking for the multiplication ofcount * size. -
Use AddressSanitizer (ASan): Compile with
-fsanitize=addressduring development to catch use-after-free at runtime. -
Static Analysis Integration: Tools like Semgrep can catch these patterns before they reach production. The rule
c.lang.security.use-after-free.use-after-freespecifically targets this class of bug.
Relevant Standards
- CWE-416: Use After Free
- CERT C Rule MEM30-C: Do not access freed memory
- OWASP: Memory management vulnerabilities in native code
Key Takeaways
- Never dereference a freed pointer, even in
sizeofexpressions — while often resolved at compile time, it's technically undefined behavior and static analyzers will rightfully flag it. - The
sizeof(galloc->leaf_allocs[0])pattern is dangerous afterfree(galloc->leaf_allocs)— usesizeof(struct leaf_alloc)instead to reference the type directly. - Setting
galloc->leaf_allocs = NULLbetweenfree()andcalloc()converts a silent use-after-free into a detectable crash — this is a critical defensive hardening practice. - Replacing
mallocwithcallocforhash_valueseliminates uninitialized memory bugs — zero-initialization is almost always worth the negligible performance cost in security-sensitive code. - Speech-to-text processing pipelines handle untrusted input (audio files) — memory safety bugs in these paths are directly reachable by attackers.
How Orbis AppSec Detected This
- Source: Untrusted computation graph data (
graph->n_leafs) derived from model/audio input processing in the Moonshine STT pipeline - Sink:
sizeof(galloc->leaf_allocs[0])dereference atggml-alloc.c:885afterfree(galloc->leaf_allocs)on line 883 - Missing control: No pointer nullification between
free()and reuse; sizeof expression referenced freed memory - CWE: CWE-416 (Use After Free)
- Fix: Set
galloc->leaf_allocs = NULLafter free and replacesizeof(galloc->leaf_allocs[0])withsizeof(struct leaf_alloc)to eliminate the dangling pointer dereference
Orbis AppSec automatically detected this vulnerability and opened a pull request with the fix. Try Orbis AppSec on your repositories to find and fix issues like this automatically.
Conclusion
Use-after-free vulnerabilities remain one of the most dangerous classes of memory safety bugs in C code. This specific instance in ggml-alloc.c demonstrates how even seemingly innocuous patterns like sizeof(ptr[0]) can become hazardous when the pointer has been freed. The fix is minimal — nullify the pointer, use the type name directly — but the security improvement is significant: it eliminates an exploit primitive from a code path that processes untrusted audio input on mobile devices.
For developers working with GGML or similar tensor libraries in C, remember: every free() call should be immediately followed by a NULL assignment, and sizeof expressions should never depend on potentially-invalid pointers.