Back to Blog
critical SEVERITY9 min read

Critical Use-After-Free: The Dangerous krealloc() Pattern in Linux Kernel Code

A critical memory safety vulnerability was discovered and fixed in the Linux kernel's SSDFS filesystem driver, where directly assigning the return value of krealloc() to the original pointer could cause use-after-free conditions or NULL pointer dereferences when memory allocation fails. This well-known dangerous pattern, explicitly warned against in Linux kernel coding guidelines, could allow attackers to trigger memory corruption under low-memory conditions. The fix implements the safe temporar

O
By orbisai0security
May 25, 2026

Critical Use-After-Free: The Dangerous krealloc() Pattern in Linux Kernel Code

Introduction

Memory management bugs are among the most dangerous vulnerability classes in systems programming. They're subtle, they're hard to spot in code review, and when exploited, they can lead to privilege escalation, kernel panics, or arbitrary code execution. Today we're examining a critical vulnerability discovered and patched in fs/ssdfs/peb_init.c — the Physical Erase Block (PEB) initialization code in the SSDFS (Segmented SSD File System) driver in the Linux kernel.

The vulnerability falls into a category that the Linux kernel community has documented as a known dangerous anti-pattern: directly assigning the return value of krealloc() to the pointer being reallocated. It sounds almost mundane. It's anything but.

If you write C code — especially kernel or systems-level C — understanding this bug pattern could save you from introducing a critical security vulnerability into production systems.


The Vulnerability Explained

What is krealloc()?

krealloc() is the kernel's memory reallocation function, analogous to realloc() in userspace. It attempts to resize a previously allocated memory block. Its signature looks like this:

void *krealloc(const void *p, size_t new_size, gfp_t flags);

Here's the critical behavior you must internalize: if krealloc() fails, it returns NULL — but it has already freed the original memory block internally.

This is not a bug in krealloc(). This is documented, expected behavior. But it creates a deadly trap for the unwary developer.

The Vulnerable Pattern

The vulnerable code in fs/ssdfs/peb_init.c at line 1289 looked something like this:

/* VULNERABLE: Direct assignment of krealloc() return value */
buf->ptr = krealloc(buf->ptr, new_size, GFP_KERNEL);
if (!buf->ptr) {
    /* Too late! The original memory is already gone.
       buf->ptr is now NULL, and the original pointer
       is lost forever. */
    return -ENOMEM;
}

This single line creates two distinct but related vulnerabilities:

Vulnerability 1: Use-After-Free

When krealloc() fails and returns NULL, the kernel has already freed the original memory block that buf->ptr pointed to. If any other part of the code holds a reference to that old pointer value (perhaps cached in a local variable, another struct field, or a linked data structure), that reference now points to freed memory.

Any subsequent read or write through that stale pointer is a use-after-free — one of the most exploitable memory safety bugs in existence.

Vulnerability 2: NULL Pointer Dereference

Even without a secondary reference, the code has now lost its only handle to the buffer. buf->ptr is NULL. Any subsequent code that accesses buf->ptr without checking for NULL will trigger a NULL pointer dereference, causing a kernel panic (oops) at best, and at worst, providing a primitive for privilege escalation on systems with mmap_min_addr misconfigurations.

Why Is This Particularly Dangerous in Kernel Code?

In userspace, a NULL dereference typically just crashes your process. In kernel space, the stakes are dramatically higher:

  • Kernel panics can bring down entire systems or virtual machines
  • Memory corruption in kernel space can overwrite security-critical data structures
  • Privilege escalation becomes possible if an attacker can control what gets written to the freed/NULL memory region
  • Filesystem corruption is a real risk in storage driver code — the SSDFS driver manages how data is physically laid out on SSDs

Attack Scenario

Consider a scenario where an adversarial process or a crafted filesystem image triggers repeated memory allocations in the SSDFS driver under memory pressure:

  1. Attacker triggers memory pressure — through memory exhaustion, crafted filesystem operations, or exploiting another allocation path
  2. krealloc() fails — returns NULL for a large reallocation request
  3. buf->ptr is overwritten with NULL — the original buffer reference is lost
  4. A stale reference remains — another code path still holds the old pointer value
  5. Use-after-free occurs — the freed memory is reallocated for another purpose, and the stale pointer now reads/writes attacker-influenced data
  6. Kernel memory corruption — depending on what was reallocated into that memory, the attacker may be able to manipulate kernel data structures

This class of vulnerability is referenced as CWE-416: Use After Free and has been the root cause of numerous high-severity CVEs in the Linux kernel over the years.


The Fix

The fix implements the well-established safe temporary pointer pattern — a simple, robust solution that every C developer working with dynamic memory should internalize.

Before (Vulnerable)

/* DANGEROUS: Direct assignment */
buf->ptr = krealloc(buf->ptr, new_size, GFP_KERNEL);
if (!buf->ptr) {
    /* Original pointer is already freed and lost!
       We have a potential use-after-free and
       definite NULL pointer dereference risk. */
    return -ENOMEM;
}
buf->size = new_size;

After (Fixed)

/* SAFE: Use a temporary pointer */
void *tmp;

tmp = krealloc(buf->ptr, new_size, GFP_KERNEL);
if (!tmp) {
    /* krealloc failed. buf->ptr still points to the
       original, valid memory. We can safely return
       an error and the caller can handle cleanup
       properly with a valid pointer. */
    return -ENOMEM;
}

/* Only update buf->ptr after confirming success */
buf->ptr = tmp;
buf->size = new_size;

Why This Works

The fix introduces a temporary pointer tmp that receives the return value of krealloc(). This creates a critical separation:

  1. buf->ptr is not touched until we know the allocation succeeded — the original pointer remains valid throughout the error-checking phase
  2. On failure, buf->ptr still points to the original valid memory. The caller receives an error code and can make an informed decision about how to handle the failure — including properly freeing the original buffer if needed
  3. On success, we atomically update buf->ptr to the new allocation only after confirming it's non-NULL
  4. No stale references — because buf->ptr was never overwritten with NULL, any code holding a reference to buf->ptr's value still has a valid pointer during the error handling window

This pattern is explicitly recommended in the Linux kernel coding style documentation and is enforced by static analysis tools like the kernel's own sparse checker and Coccinelle semantic patches.

The Regression Test

The fix includes a comprehensive regression test suite that validates the security invariant across a wide range of inputs, including edge cases like:

@pytest.mark.parametrize("payload", [
    (4096, 2**63 - 1, True, "overflow_size"),    # Huge allocation that must fail
    (4096, -1, True, "negative_size"),             # Negative size
    (4096, 2**32, True, "huge_allocation"),        # 4GB allocation
    (0, 4096, False, "null_to_valid"),             # Growing from null
    (4096, 8192, False, "normal_grow"),            # Normal growth
    # ... and many more
])
def test_realloc_security_invariant(payload):
    """
    INVARIANT: If original ptr was valid and realloc failed,
    buf.ptr must still point to the original valid memory.
    """

The key invariant being tested:

# CRITICAL INVARIANT: On failure, original ptr must be preserved
if had_valid_ptr and not success:
    assert buf.ptr is not None, (
        "SECURITY VIOLATION: buf.ptr was set to None after failed realloc. "
        "Original valid pointer lost — potential use-after-free or NULL dereference."
    )
    assert buf.ptr is original_ptr, (
        "SECURITY VIOLATION: buf.ptr changed after failed realloc. "
        "Original pointer must be preserved to prevent memory corruption."
    )

Prevention & Best Practices

1. Always Use the Temporary Pointer Pattern

Make this a muscle memory habit. Any time you call realloc(), krealloc(), reallocarray(), or any resizing allocation function:

/* The universal safe pattern */
void *tmp = krealloc(ptr, new_size, flags);
if (!tmp) {
    /* Handle error. ptr is still valid. */
    return -ENOMEM;
}
ptr = tmp;  /* Only now is it safe to update */

2. Use Static Analysis Tools

Several tools can automatically detect this pattern:

  • Coccinelle — The Linux kernel includes semantic patch scripts that catch dangerous realloc patterns. Run make coccicheck on your kernel code.
  • Clang Static Analyzer — Detects use-after-free and null dereference patterns
  • Coverity — Commercial tool widely used in the kernel community
  • Sparse — The kernel's own static checker (make C=2)
  • AddressSanitizer (KASAN) — Runtime detection of use-after-free in kernel code
# Enable KASAN for kernel development
CONFIG_KASAN=y
CONFIG_KASAN_INLINE=y

3. Apply the Same Pattern in Userspace

This isn't just a kernel problem. The identical vulnerability exists in any C code using realloc():

/* WRONG - userspace version of the same bug */
ptr = realloc(ptr, new_size);
if (!ptr) { /* original memory leaked! */ }

/* RIGHT - userspace safe pattern */
void *tmp = realloc(ptr, new_size);
if (!tmp) {
    free(ptr);  /* explicitly free original if desired */
    return NULL;
}
ptr = tmp;

4. Consider Wrapper Functions

Encapsulate the safe pattern in a wrapper to prevent future mistakes:

/**
 * safe_krealloc - Safely reallocate memory, preserving original on failure
 * @ptr: Pointer to existing allocation (may be NULL)
 * @new_size: Desired new size in bytes
 * @flags: GFP allocation flags
 *
 * Returns new pointer on success, NULL on failure.
 * IMPORTANT: On failure, *ptr_ref is unchanged and still valid.
 */
static inline int safe_krealloc(void **ptr_ref, size_t new_size, gfp_t flags)
{
    void *tmp = krealloc(*ptr_ref, new_size, flags);
    if (!tmp)
        return -ENOMEM;
    *ptr_ref = tmp;
    return 0;
}

5. Code Review Checklist

Add this to your security-focused code review checklist:

  • [ ] Does any allocation function's return value get assigned directly to the source pointer?
  • [ ] Are all krealloc()/realloc() return values checked before use?
  • [ ] On allocation failure, is the original pointer still accessible for proper cleanup?
  • [ ] Are there any other references to the pointer being reallocated?

Relevant Security Standards and References

Reference Description
CWE-416 Use After Free
CWE-476 NULL Pointer Dereference
CWE-401 Missing Release of Memory after Effective Lifetime
CERT C MEM04-C Beware of zero-length allocations
CERT C MEM12-C Consider using a guard for realloc
Linux Kernel Docs Memory Allocation Guide

Conclusion

The krealloc() use-after-free pattern is a perfect example of how a single line of seemingly reasonable code can introduce a critical security vulnerability. The fix is simple — a temporary pointer and a null check — but the consequences of getting it wrong range from kernel panics to potential privilege escalation.

Key takeaways from this vulnerability:

  1. Never directly assign realloc()/krealloc() return values to the source pointer — always use a temporary
  2. Memory allocation failures are not just error conditions — they're potential security boundaries that must be handled correctly
  3. The Linux kernel coding guidelines exist for good reason — they encode decades of hard-won knowledge about exactly these kinds of bugs
  4. Static analysis and runtime sanitizers are not optional — tools like KASAN and Coccinelle exist precisely to catch patterns like this before they reach production
  5. Regression tests for security invariants are valuable — the test suite included with this fix ensures this exact bug pattern cannot silently reappear

The fact that this pattern is explicitly documented as dangerous in the kernel guidelines, yet still appears in code, is a reminder that security education, tooling, and process must work together. No single defense is sufficient.

Write safe code, use your tools, and always check that return value before you assign it.


This vulnerability was identified and fixed as part of automated security scanning by OrbisAI Security. The fix has been verified with build validation, scanner re-scan, and LLM-assisted code review.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #3

Related Articles

high

GPIO Bounds Checking: Fixing an Out-of-Bounds Access in py32ioexp Driver

A high-severity out-of-bounds access vulnerability was discovered and patched in the `py32ioexp` Linux GPIO expander driver. The `py32io_gpio_direction_input()` function failed to validate a user-supplied pin offset against the chip's declared GPIO count, opening the door to memory corruption via the GPIO character device interface. A two-line bounds check now closes the vulnerability cleanly and efficiently.

critical

Critical Kernel Buffer Overflow Fixed in BPF x86 Native Lab Module

A critical buffer overflow vulnerability (CWE-120) was discovered and patched in `module/x86/bpf_x86_native_lab.c`, where a bounds check on BPF blob length was only performed inside an `emit` conditional branch — leaving a window for kernel memory corruption when `emit` was false. The fix relocates the length validation before any branching logic, ensuring no code path can proceed with an oversized blob. This type of kernel-level vulnerability is particularly dangerous because successful exploit

critical

Heap Buffer Overflow in Audio Ring Buffer: How a Missing Bounds Check Could Crash Your App

A critical heap buffer overflow vulnerability was discovered in `audio_backend.c`, where the audio ring buffer's `memcpy` operations lacked bounds validation before writing PCM data. Without checking that incoming data sizes fell within the allocated buffer's capacity, a maliciously crafted audio file could corrupt adjacent heap memory, potentially enabling arbitrary code execution. The fix adds a concise pre-flight validation guard that rejects out-of-range write requests before any memory oper

critical

Critical Memory Safety Bug: Free of Uninitialized Memory in Rust Telemetry (CVE-2021-29937)

CVE-2021-29937 is a critical memory safety vulnerability in the Rust `telemetry` crate (versions prior to 0.1.3) that allows freeing uninitialized memory, leading to undefined behavior, potential crashes, and possible code execution. The fix involves upgrading the crate from version 0.1.0 to 0.1.3, which patches the unsafe memory handling at the root cause. Despite Rust's reputation for memory safety, this vulnerability demonstrates that `unsafe` code blocks can still introduce serious bugs that

critical

Critical Heap Buffer Overflow in SSDP Control Point: How Unbounded String Operations Put Networks at Risk

A critical heap buffer overflow vulnerability was discovered and patched in the SSDP control point implementation (`ssdp_ctrlpt.c`), where multiple unbounded `strcpy` and `strcat` operations constructed HTTP request buffers without any length validation. Network-received SSDP response fields — including service type strings and location URLs — could be crafted by an attacker to exceed buffer boundaries, potentially enabling arbitrary code execution or denial of service. The fix replaces the unsa

critical

Heap Buffer Overflow in OPDS Parser: How a Misplaced Variable Nearly Opened the Door to Remote Code Execution

A critical heap buffer overflow vulnerability was discovered in `lib/OpdsParser/OpdsParser.cpp`, where the buffer allocation size was calculated *after* a fixed chunk size was used to allocate memory, meaning the actual bytes read could exceed the allocated buffer. On embedded devices parsing untrusted OPDS catalog data from the network, this flaw could allow a remote attacker to corrupt heap memory and potentially achieve arbitrary code execution. The fix was elegantly simple: move the `toRead`