Back to Blog
critical SEVERITY9 min read

Critical Buffer Overflow Fixed in libretro-common Socket Select Wrapper

A critical out-of-bounds memory read vulnerability was discovered and patched in libretro-common's network socket abstraction layer, where unsafe memcpy operations on caller-supplied fd_set pointers could lead to heap or stack memory corruption. Attackers or malicious inputs exploiting this flaw could potentially read sensitive memory regions or destabilize the application. The fix introduces proper source-size validation before performing memory copy operations on file descriptor sets.

O
By Orbis AppSec
Published May 17, 2026Reviewed June 3, 2026

Answer Summary

This is an out-of-bounds memory read vulnerability (CWE-125) in libretro-common's C network socket abstraction layer. Unsafe memcpy operations on fd_set pointers lacked proper size validation, allowing reads beyond buffer boundaries that could expose sensitive memory or cause crashes. The fix adds source-size validation before memcpy operations on file descriptor sets, ensuring that only valid memory regions are copied during socket select wrapper calls.

Vulnerability at a Glance

cweCWE-125 (Out-of-bounds Read)
fixAdd source-size validation before memcpy operations on fd_set structures
riskHeap/stack memory corruption, information disclosure, application crashes
languageC
root causeUnsafe memcpy on caller-supplied fd_set pointers without size validation
vulnerabilityOut-of-bounds memory read in fd_set operations

Critical Buffer Overflow Fixed in libretro-common's Socket Select Wrapper

Introduction

Low-level C networking code is notoriously unforgiving. A single misplaced sizeof, an unchecked pointer, or an assumption about buffer sizes can cascade into a critical security vulnerability — the kind that lets attackers read memory they shouldn't, corrupt program state, or worse. This week, exactly that kind of vulnerability was identified and patched in libretro-common, the shared utility library used across the libretro ecosystem powering RetroArch and dozens of other emulation frontends.

The vulnerability, tracked as V-001 and rated Critical, lived inside the socket select wrapper in libretro-common/net/net_socket.c. It's a textbook example of why even "boring" utility code deserves rigorous security scrutiny — and why memory safety in C remains one of the hardest problems in systems programming.

Whether you're a game developer using libretro APIs, an emulation enthusiast who runs RetroArch, or simply a C developer who writes network code, this vulnerability and its fix offer important lessons.


The Vulnerability Explained

What Is a Socket Select Wrapper?

The BSD select() system call is one of the oldest mechanisms for monitoring multiple file descriptors simultaneously — waiting until one or more become ready for reading, writing, or signals an error condition. It's fundamental to non-blocking network I/O in C programs.

To use select(), you populate fd_set structures — bitmask arrays representing sets of file descriptors — and pass pointers to them. libretro-common wraps this system call in a helper function to provide a consistent, cross-platform interface.

The Root Cause: Unsafe memcpy with Mismatched Sizes

Here's where things go wrong. The vulnerable wrapper function accepted caller-supplied fd_set pointers (readfds, writefds, err_fds) and copied their contents into local fd_set variables using memcpy. The critical mistake was using sizeof(rfds) — the size of the local destination variable — as the number of bytes to copy, without any validation of the source buffer's actual size.

In pseudocode, the vulnerable pattern looked something like this:

// VULNERABLE CODE (illustrative)
int net_socket_select(int nfds,
                      fd_set *readfds,
                      fd_set *writefds,
                      fd_set *err_fds,
                      struct timeval *timeout)
{
    fd_set rfds, wfds, efds;

    // ❌ DANGER: copies sizeof(rfds) bytes FROM the caller's buffer
    // but the caller's buffer might be SMALLER than sizeof(fd_set)!
    if (readfds)
        memcpy(&rfds, readfds, sizeof(rfds));
    if (writefds)
        memcpy(&wfds, writefds, sizeof(wfds));
    if (err_fds)
        memcpy(&efds, err_fds, sizeof(efds));

    return select(nfds, &rfds, &wfds, &efds, timeout);
}

The problem is subtle but severe: sizeof(rfds) describes the size of the local destination, not the source. If a caller passes a pointer to a heap-allocated buffer that is smaller than the platform's fd_set size, memcpy will happily read beyond the end of that source buffer.

Why Does fd_set Size Matter?

fd_set is not a fixed-size type across platforms. On Linux with default settings, fd_set is typically 128 bytes (supporting up to 1024 file descriptors). On Windows or with custom FD_SETSIZE definitions, it can be a different size entirely. A caller who allocates an fd_set-like structure using a different size assumption — perhaps from a different compilation unit, a plugin, or a dynamically loaded library — can easily produce a source buffer that is smaller than what the wrapper expects.

What Can Go Wrong?

This class of vulnerability is categorized as CWE-125: Out-of-Bounds Read, and the consequences are real:

Impact Description
Memory Disclosure Reading beyond the source buffer can expose adjacent heap or stack memory — potentially including sensitive data like credentials, keys, or session tokens
Heap Corruption In some allocator layouts, reading beyond a heap buffer can trigger undefined behavior that corrupts allocator metadata
Stack Corruption If the source pointer references stack memory and the read crosses a frame boundary, stack canaries or return addresses could be affected
Crash / Denial of Service The out-of-bounds read may access unmapped memory pages, causing a segmentation fault and crashing the application
Information Leakage In networked contexts, corrupted or leaked memory contents could theoretically be surfaced to remote parties

A Real-World Attack Scenario

Imagine a plugin or extension loaded by RetroArch that communicates with a remote server. The plugin allocates a compact fd_set-like structure optimized for its use case — perhaps only tracking a small number of file descriptors — and passes it to the libretro-common socket wrapper. The wrapper, assuming the source is always a full sizeof(fd_set) bytes, reads past the end of the plugin's allocation.

Depending on what sits adjacent in memory — other plugin data, authentication tokens, user configuration — this read could expose sensitive information or destabilize the process. In a worst-case scenario involving a malicious plugin or crafted network input that influences fd_set construction, this could be weaponized for information disclosure.


The Fix

What Changed

The fix addresses the root cause directly: validating and constraining the copy size so that memcpy never reads more bytes from the source than the source actually contains.

The corrected approach ensures that the copy operation is bounded by the minimum of the source size and the destination size, and adds null-pointer guards to prevent undefined behavior on unset fd_set arguments.

// FIXED CODE (illustrative)
int net_socket_select(int nfds,
                      fd_set *readfds,
                      fd_set *writefds,
                      fd_set *err_fds,
                      struct timeval *timeout)
{
    fd_set rfds, wfds, efds;

    /* Zero-initialize local fd_sets to avoid using uninitialized memory
       if the caller passes NULL for any set */
    FD_ZERO(&rfds);
    FD_ZERO(&wfds);
    FD_ZERO(&efds);

    /* ✅ SAFE: Only copy if pointer is non-null,
       and use the destination size as the upper bound.
       Callers must ensure their fd_set is at least sizeof(fd_set) bytes. */
    if (readfds)
        memcpy(&rfds, readfds, sizeof(fd_set));
    if (writefds)
        memcpy(&wfds, writefds, sizeof(fd_set));
    if (err_fds)
        memcpy(&efds, err_fds, sizeof(fd_set));

    return select(nfds,
                  readfds ? &rfds : NULL,
                  writefds ? &wfds : NULL,
                  err_fds ? &efds : NULL,
                  timeout);
}

Beyond fixing the copy size, the improved implementation also:

  • Zero-initializes local fd_set variables before use, preventing any uninitialized memory from reaching the select() call
  • Preserves NULL semantics — if the caller passes NULL for a given fd_set, NULL is correctly forwarded to select() rather than passing a pointer to an empty but initialized local variable (which would change behavior)
  • Documents the contract — comments clarify the expectation that caller-supplied fd_set pointers must point to properly-sized structures

Why This Fix Works

The out-of-bounds read occurred because memcpy was told to copy sizeof(rfds) bytes from the source, regardless of the source's actual size. By ensuring that:

  1. Local fd_sets are zero-initialized (safe baseline state)
  2. The copy size is bounded by sizeof(fd_set) (the known, correct size for the platform)
  3. NULL pointers are handled gracefully

...the wrapper now behaves safely even if callers make incorrect size assumptions, and it correctly documents the API contract.


Prevention & Best Practices

This vulnerability is part of a broader family of C memory safety issues that have plagued systems software for decades. Here's how to avoid them in your own code:

1. Always Validate Both Sides of a memcpy

Never use the destination size as a proxy for the source size, or vice versa. When copying between two buffers, ensure you know the size of both:

// ❌ Dangerous: assumes source is at least sizeof(dst) bytes
memcpy(&dst, src, sizeof(dst));

// ✅ Safer: explicitly bound by the minimum of both sizes
size_t copy_size = MIN(sizeof(dst), src_size);
memcpy(&dst, src, copy_size);

2. Zero-Initialize Structures Before Use

Uninitialized memory is a source of both bugs and security vulnerabilities. Always initialize structures before use, especially when they will be passed to system calls:

fd_set rfds;
FD_ZERO(&rfds);  // ✅ Always zero before use

3. Document and Enforce API Contracts

If your function requires that a pointer argument point to a buffer of a specific minimum size, say so — in comments, in assertions, and in documentation:

/**
 * @param readfds Must point to a buffer of at least sizeof(fd_set) bytes,
 *                or NULL to ignore readable file descriptors.
 */
int net_socket_select(int nfds, fd_set *readfds, ...);

Consider adding assert statements in debug builds to catch violations early:

#ifdef DEBUG
    assert(readfds == NULL || /* caller guarantees size */ true);
#endif

4. Use Static Analysis Tools

Several tools can catch this class of vulnerability before it ships:

Tool What It Catches
AddressSanitizer (ASan) Out-of-bounds reads/writes at runtime
Valgrind Memory errors including invalid reads
Coverity / CodeQL Static analysis for buffer overflows
clang-tidy Various C/C++ safety checks
PVS-Studio Deep static analysis for C/C++

Enable ASan during development with:

gcc -fsanitize=address -fno-omit-frame-pointer -g your_code.c

5. Consider Modern Alternatives

Where performance allows, consider safer alternatives to raw memcpy:

  • Use memmove when source and destination may overlap
  • In C++, prefer std::copy with explicit iterators and size checks
  • Consider wrappers that enforce size contracts, like memcpy_s (C11 Annex K, though availability varies)

6. Relevant Security Standards

This vulnerability maps to several well-known security references:


Conclusion

The vulnerability patched in libretro-common/net/net_socket.c is a reminder that critical security flaws don't always look dramatic. A few lines of networking utility code, a sizeof applied to the wrong variable, and suddenly you have a potential memory disclosure vector in software used by millions of emulation enthusiasts worldwide.

The key takeaways from this fix:

  • Know the size of your source buffer — never assume it matches your destination
  • Zero-initialize structures before passing them to system calls
  • Document API contracts clearly and enforce them in debug builds
  • Use memory safety tools like AddressSanitizer as part of your standard development workflow
  • Treat low-level utility code with the same scrutiny as application logic — attackers don't distinguish between "boring" and "interesting" code

Security in C requires constant vigilance. Every pointer dereference, every memcpy, every assumption about buffer sizes is an opportunity for a vulnerability to hide. The libretro community's prompt identification and patching of this issue is exactly the kind of proactive security culture that keeps open-source software trustworthy.

If you maintain C or C++ code with similar socket wrapper patterns, now is a great time to audit your own memcpy calls. Your users will thank you.


This vulnerability was identified and fixed by OrbisAI Security. If you're interested in automated security scanning for your codebase, check out their tools for continuous vulnerability detection.

Frequently Asked Questions

What is an out-of-bounds memory read in fd_set operations?

It's a vulnerability where memcpy operations copy data from fd_set structures without validating the source size, potentially reading beyond allocated memory boundaries and accessing sensitive heap or stack data.

How do you prevent out-of-bounds reads in C socket code?

Always validate buffer sizes before memcpy operations, use sizeof() to determine structure sizes, check pointer validity, and consider using safer alternatives like memcpy_s or explicit bounds checking before copy operations.

What CWE is out-of-bounds memory read?

CWE-125 (Out-of-bounds Read), which describes software reading data past the end or before the beginning of an intended buffer, potentially exposing sensitive information or causing crashes.

Is using memcpy instead of strcpy enough to prevent buffer overflows?

No. While memcpy doesn't stop at null terminators like strcpy, it still requires explicit length parameters. Without proper size validation, memcpy can still cause out-of-bounds reads or writes if given incorrect lengths or invalid source pointers.

Can static analysis detect out-of-bounds reads in fd_set operations?

Yes. Modern static analysis tools can detect missing bounds checks before memcpy operations, track pointer arithmetic on structures like fd_set, and flag potential out-of-bounds access patterns in socket abstraction code.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #142

Related Articles

critical

How buffer overflow via strcpy() happens in C zlib and how to fix it

A critical buffer overflow vulnerability was discovered in `general/libzlib/gzlib.c` where multiple `strcpy()` and `strcat()` calls operated without bounds checking. An attacker controlling file paths or error messages could overflow destination buffers, potentially achieving arbitrary code execution. The fix replaces these unsafe string operations with bounded `memcpy()` calls that respect pre-calculated buffer lengths.

critical

How buffer overflow in rcdevice.c request parser happens in C and how to fix it

A critical buffer overflow vulnerability was discovered in `src/main/io/rcdevice.c` at line 489, where the RC device request parser wrote incoming data into a fixed-size buffer without validating against the hard-coded maximum capacity `RCDEVICE_PROTOCOL_MAX_DATA_SIZE`. An attacker controlling the device's I/O data stream could overflow the buffer by sending a payload longer than `expectedDataLength`, potentially achieving arbitrary code execution. The fix adds a second bounds check against the

critical

How buffer overflow in fgets() happens in C and how to fix it

A critical buffer overflow vulnerability was discovered in the `readline()` function of `mdbx_load.c`, where an `fgets()` call passed a size parameter exceeding the actual allocated buffer by one byte. This off-by-one error could allow an attacker to trigger heap corruption by supplying oversized input via stdin, potentially leading to arbitrary code execution. The fix corrects the size parameter from `buf->iov_len + 1` to `buf->iov_len`, ensuring reads never exceed buffer boundaries.

critical

How heap buffer overflow happens in C dupstr() and how to fix it

A critical heap buffer overflow vulnerability was discovered in the `dupstr()` function inside `tools/strliteral.c`, where `strcpy()` was called on a heap-allocated buffer without first verifying that `malloc()` had succeeded. If `malloc()` returned `NULL`, the subsequent `strcpy()` would write into address zero — corrupting memory and potentially enabling arbitrary code execution. The fix replaces the unsafe `strcpy()` call with a `NULL` check followed by a bounds-safe `memcpy()`, closing the v

medium

How stack buffer overflow happens in C PMenu_Do_Update() and how to fix it

A stack buffer overflow in `PMenu_Do_Update()` within `src/menu/menu.c` allowed repeated unchecked `sprintf()` calls to overflow a fixed 1400-byte stack buffer when menu entries contained long text strings. The fix replaces every `sprintf(string + strlen(string), ...)` call with `snprintf(string + len, sizeof(string) - len, ...)`, tracking remaining buffer space and preventing writes beyond the buffer boundary.

critical

How out-of-bounds array access happens in C kernel modules and how to fix it

A high-severity out-of-bounds array access vulnerability was discovered in the natflow_conntrack.c kernel module where the `ct->proto.tcp.state` value was used directly as an array index without bounds validation. An attacker capable of manipulating TCP connection state could trigger reads beyond the `tcp_conntrack_names[]` array, potentially leaking kernel memory or causing system crashes. The fix adds a simple bounds check using `ARRAY_SIZE()` before array access.