Back to Blog
high SEVERITY6 min read

How c.lang.security.use-after-free.use-after-free happens in C and how to fix it

A use-after-free vulnerability was discovered in `ggml-alloc.c` where `galloc->leaf_allocs` could be referenced after being freed during graph memory reallocation. The fix nullifies the pointer immediately after `free()` and uses explicit `sizeof(struct leaf_alloc)` to prevent undefined behavior. This defensive hardening eliminates an exploit primitive in a speech-to-text processing pipeline.

O
By Orbis AppSec
•Technically reviewed by Anupam Mediratta•Published August 22, 2026•Reviewed August 22, 2026

Answer Summary

This is a use-after-free vulnerability (CWE-416) in C within the `ggml_gallocr_reserve_n_impl` function of `ggml-alloc.c`. The variable `galloc->leaf_allocs` was freed and then immediately used in the `sizeof` expression of the subsequent `calloc` call on line 885. The fix sets the pointer to NULL after freeing and replaces `sizeof(galloc->leaf_allocs[0])` with `sizeof(struct leaf_alloc)` to avoid dereferencing the freed pointer.

Vulnerability at a Glance

cweCWE-416
fixSet pointer to NULL after free, use `sizeof(struct leaf_alloc)` instead of `sizeof(galloc->leaf_allocs[0])`
riskUndefined behavior leading to potential code execution or memory corruption
languageC
root cause`galloc->leaf_allocs` dereferenced in `sizeof` expression after being freed on the preceding line
vulnerabilityUse-After-Free

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:

  1. Line 883: free(galloc->leaf_allocs) — the memory is deallocated
  2. 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:

  1. An attacker crafts a malicious audio input that causes the computation graph to be resized (triggering graph->n_leafs > galloc->n_leafs)
  2. Between the free() and the calloc(), if the freed memory is reclaimed (e.g., by another thread or an allocator that coalesces immediately), the sizeof expression could evaluate to an attacker-controlled value on implementations where it's not resolved at compile time
  3. This could lead to an undersized allocation, followed by out-of-bounds writes when the leaf_allocs array 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:

  1. galloc->leaf_allocs = NULL — Immediately nullifies the dangling pointer after free(). If any code path accidentally accesses it before reallocation, it will trigger a clean NULL pointer dereference (detectable crash) rather than silent undefined behavior.

  2. sizeof(struct leaf_alloc) instead of sizeof(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.

Key Takeaways

  • Never dereference a freed pointer, even in sizeof expressions — 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 after free(galloc->leaf_allocs) — use sizeof(struct leaf_alloc) instead to reference the type directly.
  • Setting galloc->leaf_allocs = NULL between free() and calloc() converts a silent use-after-free into a detectable crash — this is a critical defensive hardening practice.
  • Replacing malloc with calloc for hash_values eliminates 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 at ggml-alloc.c:885 after free(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 = NULL after free and replace sizeof(galloc->leaf_allocs[0]) with sizeof(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.

Prevention and further reading

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #45

Related Articles

critical

Lampa Desktop Auto-Update Heuristic Bypass: Execution of Unverified

Lampa Desktop's auto-update mechanism downloaded JavaScript and CSS from `raw.githubusercontent.com` using only heuristic validation—file size thresholds and string pattern matching—that attackers could trivially satisfy. The fix introduces cryptographic integrity verification by cross-referencing Git blob hashes from the GitHub Contents API, ensuring downloaded code matches the repository's authoritative state before execution.

high

adm-zip 0.6.0 Preserves SUID Bits From ZIPs: CVE-2026-102282

The `adm-zip` dependency resolved to 0.6.0 in this project's dependency tree, a version affected by CVE-2026-102282: during extraction it applies the Unix permission bits stored in each ZIP entry's external file attributes verbatim, including the setuid (`04000`), setgid (`02000`), and sticky bits. An attacker who controls an archive passed to `extractAllTo()` or `extractEntryTo()` can therefore have the extractor create a setuid binary owned by whatever user the extraction process runs as. The

high

requestInput() Type Confusion: NaN and Object Bypass in JavaScript

The `requestInput()` utility function lacked validation on its `type` parameter and failed to handle `NaN` results from float conversions, creating a type confusion weakness. An attacker could supply malformed inputs that propagate unhandled `NaN` values or unexpected object types through the type system. The fix adds explicit guards against `NaN` type parameters and rejects non-primitive type values.

critical

No Rate Limit on /api/uploads/presign Enables DoS

The `/api/uploads/presign` endpoint accepted unlimited concurrent requests to generate storage presigned URLs, giving an attacker a free lever to exhaust storage-provider quotas and server resources. The fix adds an `express-rate-limit` middleware capping each client to 30 requests per minute on that route.

high

CVE-2026-54673: builder-util-runtime Leaks Auth Headers on Redirect

electron-updater and electron-builder rely on builder-util-runtime to fetch update manifests and artifacts over HTTP. A flaw in that shared HTTP executor allowed credential headers attached to the original update-feed request to be re-sent after a redirect, exposing them to any host the redirect pointed to. The project fixes this by upgrading builder-util-runtime to 9.7.0 and collapsing a duplicate, older copy of the package that electron-updater had pinned on its own.

high

image-size 1.2.1 DoS: Zero-Valued Dimensions in Image Buffer Parser

A high-severity denial-of-service vulnerability in image-size 1.2.1 allows attackers to crash Node.js services using malicious image buffers with zero-valued dimensions. The fix removes the vulnerable `queue` dependency and tightens dimension validation in version 2.0.3.