home intel cve-2025-38618-vsock-bind-vmaddr-port-any-uaf
CVE Analysis 2025-08-22 · 8 min read

CVE-2025-38618: vsock VMADDR_PORT_ANY Autobind Use-After-Free

A use-after-free in Linux vsock allows an autobiound socket accepted via accept() to trigger an extra refcount decrement when bound to VMADDR_PORT_ANY, corrupting socket lifecycle state.

#use-after-free#vsock#reference-counting#linux-kernel#memory-corruption
Technical mode — for security professionals
▶ Attack flow — CVE-2025-38618 · Use After Free
ATTACKERRemote / unauthUSE AFTER FREECVE-2025-38618Linux · HIGHCODE EXECArbitrary coderuns as targetCOMPROMISEFull accessNo confirmed exploits

Vulnerability Overview

CVE-2025-38618 is a use-after-free in the Linux kernel's vsock subsystem, introduced by the autobind-to-VMADDR_PORT_ANY path. When a listening socket autobinds to VMADDR_PORT_ANY (0xFFFFFFFF) and a client connects, the socket returned by accept() inherits VMADDR_PORT_ANY as its local port. That accepted socket is not on the unbound sockets list, but the missing guard in __vsock_bind_connectible() allows a subsequent bind() call on it to proceed — decrementing the socket's refcount an extra time and producing a UAF primitive on the vsock_sock object.

CVSS 7.8 (local, high severity). No in-the-wild exploitation reported. Requires the ability to create and manipulate AF_VSOCK sockets, which is available to unprivileged users inside a VM guest.

Affected Component

Subsystem: net/vmw_vsock/af_vsock.c
Function: __vsock_bind_connectible(), vsock_autobind(), vsock_accept()
Kernel versions: See NVD. Regression relative to the fix in fcdd2242c023 ("vsock: Keep the binding until socket destruction").

Root Cause Analysis

The bug lives at the intersection of two behaviors: (1) autobind assigns VMADDR_PORT_ANY as a sentinel meaning "no port yet assigned", and (2) __vsock_bind_connectible() previously only blocked re-binding sockets already on the bound list, not sockets carrying the VMADDR_PORT_ANY sentinel.

The autobind path for a connecting socket:

/* net/vmw_vsock/af_vsock.c */
static int vsock_autobind(struct vsock_sock *vsk)
{
    struct sockaddr_vm local;

    /* Assign VMADDR_PORT_ANY — transport will allocate a real port later */
    vsock_addr_init(&local, VMADDR_CID_ANY, VMADDR_PORT_ANY);
    /* BUG: this marks the socket as "bound" with port == 0xFFFFFFFF */
    return __vsock_bind(sock_net(sk_vsock(vsk)),
                        vsk, &local);
}

The vulnerable guard in __vsock_bind_connectible() before the patch:

static int __vsock_bind_connectible(struct vsock_sock *vsk,
                                    struct sockaddr_vm *addr)
{
    /* Retrieve the global unbound-sockets list lock */
    spin_lock_bh(&vsock_table_lock);

    /*
     * Only reject rebind if the socket is already on the bound list.
     * BUG: does NOT reject VMADDR_PORT_ANY — an accept()ed socket
     *      that inherited port 0xFFFFFFFF will pass this check even
     *      though it was never placed on the unbound list.
     */
    if (!list_empty(&vsk->bound_table)) {        // BUG: insufficient guard
        spin_unlock_bh(&vsock_table_lock);
        return -EINVAL;
    }

    /* Remove from unbound list — decrements refcount */
    __vsock_remove_bound(vsk);                   // BUG: called on a socket
                                                 //      not on the list;
                                                 //      extra sock_put() here
    vsock_insert_bound(vsk, addr->svm_port);
    spin_unlock_bh(&vsock_table_lock);
    return 0;
}

The accept() path that produces the dangling socket:

static int vsock_accept(struct socket *sock, struct socket *newsock, int flags,
                        bool kern)
{
    struct vsock_sock *vconnected;
    /* ... wait for connection ... */

    vconnected = vsock_sk(connected);
    /*
     * The accepted socket inherits the listener's local port.
     * If the listener autobiound to VMADDR_PORT_ANY, the accepted
     * socket's svm_local.svm_port == VMADDR_PORT_ANY (0xFFFFFFFF).
     * It is NOT added to vsock_unbound_sockets — only fully bound
     * sockets go on vsock_bound_table.
     */
    vsock_addr_init(&vconnected->local_addr,
                    vconnected->local_addr.svm_cid,
                    vsk->local_addr.svm_port); /* inherits 0xFFFFFFFF */

    /* accepted socket is never inserted into unbound list */
    return 0;
}
Root cause: __vsock_bind_connectible() failed to reject VMADDR_PORT_ANY as a bind target, allowing an accept()-returned socket that inherited the sentinel port to trigger __vsock_remove_bound() on a socket never placed on the unbound list, producing an extra sock_put() and a use-after-free on the vsock_sock object.

Struct Layout

/* include/net/af_vsock.h — relevant fields */
struct vsock_sock {
    /* +0x00 */ struct sock          sk;           // embedded sock (0xE8 bytes)
    /* +0xe8 */ struct sockaddr_vm   local_addr;   // svm_port at +0xec
    /* +0xf8 */ struct sockaddr_vm   remote_addr;
    /* +0x108 */ struct list_head    bound_table;  // node on vsock_bound_table
    /* +0x118 */ struct list_head    connected_table;
    /* +0x128 */ bool                trusted;
    /* +0x129 */ bool                cached_peer_allow_dgram;
    /* +0x12c */ u32                 peer_shutdown;
    /* +0x130 */ struct vsock_transport *transport;
    /* ...    */ /* transport-private state follows */
};

struct sockaddr_vm {
    /* +0x00 */ sa_family_t   svm_family;   // AF_VSOCK
    /* +0x02 */ u16           svm_reserved; // must be zero
    /* +0x04 */ u32           svm_port;     // 0xFFFFFFFF == VMADDR_PORT_ANY
    /* +0x08 */ u32           svm_cid;
    /* +0x0c */ u8            svm_flags;
    /* +0x0d */ u8            svm_zero[3];
};

Memory Layout

LIFECYCLE — NORMAL BOUND SOCKET:
  socket()  -> vsock_sock allocated, sk.refcnt = 1
  bind()    -> __vsock_remove_bound() (from unbound list, refcnt OK)
              vsock_insert_bound()
  close()   -> sock_put() -> refcnt = 0 -> vsock_sk_destruct() -> kfree

LIFECYCLE — VULNERABLE AUTOBIND + ACCEPT PATH:

  [listener]  socket()        refcnt=1, port=VMADDR_PORT_ANY
              autobind()      port stays 0xFFFFFFFF, NOT on unbound list
              listen()
              accept()  ──────────────────────────────────────────────┐
                                                                       │
  [accepted]  vsock_sock allocated, refcnt=1                          │
              inherits svm_port = 0xFFFFFFFF ◄────────────────────────┘
              NOT inserted into vsock_unbound_sockets

  [attacker]  bind(accepted_fd, port=VMADDR_PORT_ANY)
                -> __vsock_bind_connectible()
                -> list_empty(&vsk->bound_table) == true  (passes check)
                -> __vsock_remove_bound(vsk)
                     -> list_del(&vsk->bound_table)   // UB: not on list
                     -> sock_put(sk_vsock(vsk))        // EXTRA decrement
                        refcnt: 1 -> 0
                -> vsock_sk_destruct() called EARLY
                -> vsock_sock object FREED  <── UAF object

  [later]     any operation on accepted_fd dereferences freed vsock_sock
              OR transport layer callback fires on dangling pointer

SLAB STATE AFTER EARLY FREE:
  [ kmalloc-vsock_sock slab ]
    slot N:  [ FREED vsock_sock ] <- refilled by next allocation
                                     subsequent sock ops = type confusion / UAF

Exploitation Mechanics

EXPLOIT CHAIN (local privilege escalation from unprivileged VM guest):

1. Open AF_VSOCK socket, set SO_REUSEADDR.
   sockfd = socket(AF_VSOCK, SOCK_STREAM, 0);

2. Bind listener to VMADDR_PORT_ANY:
   addr.svm_port = VMADDR_PORT_ANY;  // 0xFFFFFFFF
   bind(sockfd, &addr, sizeof(addr));
   // autobind fires: port stays 0xFFFFFFFF, socket NOT on unbound list

3. listen(sockfd, 5);

4. From a second thread/process: connect() to the listener's CID.
   // Triggers vsock_accept() path in kernel

5. accept(sockfd, ...) -> acceptfd
   // acceptfd->vsock_sock.local_addr.svm_port = 0xFFFFFFFF
   // acceptfd's vsock_sock is NOT on any list

6. bind(acceptfd, VMADDR_PORT_ANY):
   // Passes list_empty() guard (bound_table is empty)
   // __vsock_remove_bound() -> sock_put() -> refcnt hits 0
   // vsock_sk_destruct() fires -> vsock_sock slab slot freed

7. Heap spray: fill freed slab slot with controlled data
   // e.g., via sendmsg() on another socket to groom kmalloc-N cache
   // or via io_uring fixed buffer registration if slab sizes align

8. Trigger transport callback (e.g., virtio_transport_recv_pkt) on
   acceptfd — now dereferences attacker-controlled vsock_sock.transport
   pointer -> arbitrary function pointer call

9. Pivot to kernel RIP via fake transport->ops->release or
   ->notify_send_pre function pointer embedded in sprayed data.

Patch Analysis

// BEFORE (vulnerable): net/vmw_vsock/af_vsock.c
static int __vsock_bind_connectible(struct vsock_sock *vsk,
                                    struct sockaddr_vm *addr)
{
    spin_lock_bh(&vsock_table_lock);

    if (!list_empty(&vsk->bound_table)) {
        // Only blocks re-bind if already on bound list.
        // Does NOT block VMADDR_PORT_ANY as a bind target.
        spin_unlock_bh(&vsock_table_lock);
        return -EINVAL;
    }

    __vsock_remove_bound(vsk);
    vsock_insert_bound(vsk, addr->svm_port);
    spin_unlock_bh(&vsock_table_lock);
    return 0;
}

// AFTER (patched):
static int __vsock_bind_connectible(struct vsock_sock *vsk,
                                    struct sockaddr_vm *addr)
{
    spin_lock_bh(&vsock_table_lock);

    if (!list_empty(&vsk->bound_table)) {
        spin_unlock_bh(&vsock_table_lock);
        return -EINVAL;
    }

    // NEW GUARD: explicitly reject VMADDR_PORT_ANY as a bind target.
    // Prevents an accept()ed socket that inherited port 0xFFFFFFFF
    // from passing through to __vsock_remove_bound().
    if (addr->svm_port == VMADDR_PORT_ANY) {
        spin_unlock_bh(&vsock_table_lock);
        return -EINVAL;
    }

    __vsock_remove_bound(vsk);
    vsock_insert_bound(vsk, addr->svm_port);
    spin_unlock_bh(&vsock_table_lock);
    return 0;
}

The fix is minimal and surgical: a single sentinel check before the unbound-list removal path. It mirrors the spirit of fcdd2242c023, which preserved binding across the socket lifetime to prevent analogous extra-sock_put() scenarios. The new guard closes the gap where VMADDR_PORT_ANY acted as a bypass token for the existing list_empty check.

Detection and Indicators

Kernel KASAN / KFENCE output — look for UAF reports in vsock_sk_destruct or vsock_transport dereference paths:

==================================================================
BUG: KASAN: use-after-free in vsock_sk_destruct+0x...
Read of size 8 at addr ffff888... by task poc/...

CPU: X PID: Y Comm: poc
Call Trace:
  vsock_sk_destruct
  __sk_destruct
  sk_free
  sock_put                   <-- double sock_put
  __vsock_bind_connectible
  vsock_bind
  __sys_bind
==================================================================

Audit syscall sequence to flag:

socket(AF_VSOCK, SOCK_STREAM, 0)
bind(fd, {svm_port=VMADDR_PORT_ANY}, ...)
listen(fd, N)
accept(fd, ...)              -> acceptfd
bind(acceptfd, {svm_port=VMADDR_PORT_ANY}, ...)   <-- anomalous

The final bind() on the accepted socket with VMADDR_PORT_ANY is the trigger. No legitimate application performs this sequence; flag it as anomalous in seccomp/audit policy.

Remediation

  • Patch immediately: Apply the upstream fix that adds the VMADDR_PORT_ANY guard to __vsock_bind_connectible(). Verify your distribution kernel includes the commit.
  • Mitigate at the module level: If vsock is not required in the guest, unload vmw_vsock_virtio_transport / vmw_vsock_vmci_transport or add install vsock /bin/false to modprobe config.
  • Unprivileged user namespaces: Restricting AF_VSOCK socket creation to privileged users via LSM policy (SELinux socket vsock_socket create deny for unconfined users) reduces attack surface.
  • Enable KFENCE/KASAN in staging/test kernels to catch any related UAF regressions in the vsock bind/autobind path before promoting to production.
  • Audit vsock usage: Review whether guest workloads require VMADDR_PORT_ANY autobind semantics at all; if not, a local seccomp filter blocking bind(AF_VSOCK, port=0xFFFFFFFF) provides defense-in-depth.
CB
CypherByte Research
Mobile security intelligence · cypherbyte.io
// RELATED RESEARCH
// WEEKLY INTEL DIGEST

Get articles like this every Friday — mobile CVEs, threat research, and security intelligence.

Subscribe Free →