Skip to content

[Vulner]: Unbounded Memory Allocation in vSomeIP TCP Server Endpoint #1009

@pic7oris

Description

@pic7oris

vSomeip Version

v3.6.1

Boost Version

1.71

Environment

ubuntu20.04

Describe the bug

1. Overview

Field Value
Affected Software vSomeIP 3.6.1 (COVESA/vsomeip)
Component TCP Server Endpoint (tcp_server_endpoint_impl)
CWE CWE-789: Memory Allocation with Excessive Size Value
CVSS 3.1 Vector AV:P/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H
CVSS 3.1 Score 4.6 (MEDIUM)
Attack Vector Physical — requires access to the in-vehicle Ethernet bus
Authentication None required
Impact Denial of Service via memory exhaustion

2. CVSS Scoring Rationale

Metric Value Justification
Attack Vector Physical (P) SOME/IP operates on in-vehicle Ethernet. The attacker must physically connect to the vehicle network (e.g., via OBD-II port or direct Ethernet bus tap).
Attack Complexity Low (L) No race conditions, no special configuration required. Default configuration is vulnerable.
Privileges Required None (N) The TCP endpoint performs no authentication on incoming connections.
User Interaction None (N) No user action required.
Scope Unchanged (U) Impact is confined to the vSomeIP process.
Confidentiality None (N) No information disclosure.
Integrity None (N) No data modification.
Availability High (H) The target process can be forced to consume all available system memory, leading to OOM termination or system-wide resource starvation.

Note on Attack Vector: If the SOME/IP endpoint is reachable through a Telematics Control Unit (T-Box) or gateway with network bridging, the Attack Vector should be reclassified as Adjacent (A) or Network (N), raising the score to 6.5 or 7.5 respectively.

3. Vulnerability Location

Primary File

implementation/endpoints/src/tcp_server_endpoint_impl.cpp

Code Path Line(s) Description
receive_cbk() 586 get_message_size() reads the Length field from attacker-controlled data
receive_cbk() 591 Truncates 64-bit size to uint32_t (current_message_size)
receive_cbk() 719 max_message_size_ check — bypassed because default is MESSAGE_SIZE_UNLIMITED
receive_cbk() 740–741 Computes missing_capacity_ directly from attacker-controlled current_message_size
receive() 449–453 Allocates buffer of size recv_buffer_size_ + missing_capacity_no upper bound

Supporting Files

File Line(s) Description
implementation/utility/src/utility.cpp 48–53 get_message_size(): returns VSOMEIP_SOMEIP_HEADER_SIZE (8) + Length_field
implementation/configuration/include/internal.hpp 170 MESSAGE_SIZE_UNLIMITED = std::numeric_limits<uint32_t>::max() (0xFFFFFFFF)
implementation/configuration/src/configuration_impl.cpp 3397 get_max_message_size_reliable() returns MESSAGE_SIZE_UNLIMITED when no max is configured

4. Root Cause Analysis

4.1 Data Flow

The SOME/IP protocol header is 16 bytes:

Byte  0- 1: Service ID
Byte  2- 3: Method ID
Byte  4- 7: Length          ← attacker-controlled
Byte  8- 9: Client ID
Byte 10-11: Session ID
Byte    12: Protocol Version
Byte    13: Interface Version
Byte    14: Message Type
Byte    15: Return Code

The Length field (bytes 4–7) specifies the number of bytes following the first 8 bytes of the header. The total message size is therefore 8 + Length.

4.2 Vulnerable Code Path

Step 1receive_cbk() reads the attacker-supplied Length field:

// tcp_server_endpoint_impl.cpp, line 586
uint64_t read_message_size =
    utility::get_message_size(&recv_buffer_[its_iteration_gap], recv_buffer_size_);

get_message_size() in utility.cpp line 48–53:

uint64_t utility::get_message_size(const byte_t* _data, size_t _size) {
    uint64_t its_size(0);
    if (VSOMEIP_SOMEIP_HEADER_SIZE <= _size) {
        its_size = VSOMEIP_SOMEIP_HEADER_SIZE
                 + bithelper::read_uint32_be(&_data[4]);  // reads Length field
    }
    return its_size;
}

For an attacker sending Length = 0x04000000 (64 MB):

  • read_message_size = 8 + 67,108,864 = 67,108,872

Step 2 — The size is truncated to uint32_t and checked against max_message_size_:

// line 591
uint32_t current_message_size = static_cast<uint32_t>(read_message_size);
// current_message_size = 67,108,872

// line 719 — this check is BYPASSED because max_message_size_ == MESSAGE_SIZE_UNLIMITED
} else if (max_message_size_ != MESSAGE_SIZE_UNLIMITED
           && current_message_size > max_message_size_) {
    // ... close connection (never reached with default config)

Since max_message_size_ defaults to MESSAGE_SIZE_UNLIMITED (0xFFFFFFFF), the condition max_message_size_ != MESSAGE_SIZE_UNLIMITED evaluates to false, and the entire size limit check is skipped.

Step 3missing_capacity_ is set from attacker-controlled data:

// line 740-741
} else if (current_message_size > recv_buffer_size_) {
    missing_capacity_ = current_message_size
                      - static_cast<std::uint32_t>(recv_buffer_size_);
}
// missing_capacity_ = 67,108,872 - 16 = 67,108,856 (~64 MB)

Step 4receive() allocates the buffer unconditionally:

// tcp_server_endpoint_impl.cpp, line 444-453
if (missing_capacity_) {
    if (missing_capacity_ > MESSAGE_SIZE_UNLIMITED) {  // always false for uint32_t
        return;
    }
    const std::size_t its_required_capacity(recv_buffer_size_ + missing_capacity_);
    if (its_capacity < its_required_capacity) {
        recv_buffer_.reserve(its_required_capacity);
        recv_buffer_.resize(its_required_capacity, 0x0);  // ALLOCATES 64 MB
    }
}

The check at line 445 (missing_capacity_ > MESSAGE_SIZE_UNLIMITED) is effectively a no-op: missing_capacity_ is uint32_t and MESSAGE_SIZE_UNLIMITED is uint32_t::max(), so this condition can only be true for missing_capacity_ == 0 edge cases. For any valid allocation request, it passes through.

4.3 Why the Existing Size Check Does Not Help

The existing guard at line 719:

if (max_message_size_ != MESSAGE_SIZE_UNLIMITED && current_message_size > max_message_size_)

This check is dead code under default configuration because:

  1. max_message_size_ is initialized from configuration_->get_max_message_size_reliable() (line 102)
  2. Without explicit configuration, this returns MESSAGE_SIZE_UNLIMITED (configuration_impl.cpp line 3397)
  3. The first sub-condition max_message_size_ != MESSAGE_SIZE_UNLIMITED is false
  4. Short-circuit evaluation skips the size comparison entirely

4.4 Amplification Factor

Attack Input Server Allocation Amplification
16 bytes (one SOME/IP header) 64 MB 4,194,304 : 1
128 bytes (8 connections) 512 MB 4,194,304 : 1
256 bytes (16 connections) 1,024 MB 4,194,304 : 1

Each 16-byte SOME/IP header with Length = 0x04000000 causes a 64 MB allocation. The amplification ratio is constant regardless of the number of connections.

5. Impact Analysis

5.1 Direct Impact

  • Memory Exhaustion: The attacker can force arbitrary memory allocation. With 16 concurrent connections each claiming 256 MB, the server process allocates 4 GB.
  • OOM Termination: On systems with limited memory, the Linux OOM killer will terminate the vSomeIP process.
  • System-wide Starvation: On embedded automotive ECUs with limited RAM (typically 512 MB–2 GB), even a few malicious connections can starve all other processes.

5.2 Automotive Context

  • Service Disruption: If the vSomeIP routing manager is killed by OOM, all SOME/IP services on that ECU become unavailable.
  • No Recovery Without Restart: The process must be manually restarted (or by a watchdog). During downtime, safety-critical communication may be interrupted.
  • Silent Attack: The allocation happens silently — vSomeIP only logs an info-level message when the buffer exceeds 1 MB (line 454), which is easily missed.

6. Suggested Fix

Option A: Enforce a Hard Maximum Message Size (Recommended)

// In receive_cbk(), before line 740:
constexpr uint32_t VSOMEIP_MAX_TCP_MESSAGE_SIZE_DEFAULT = 64 * 1024; // 64 KB

if (current_message_size > VSOMEIP_MAX_TCP_MESSAGE_SIZE_DEFAULT) {
    VSOMEIP_ERROR << instance_name_ << __func__
                  << ": message size " << current_message_size
                  << " exceeds maximum allowed size. Closing connection.";
    its_lock.unlock();
    wait_until_sent(boost::asio::error::operation_aborted);
    return;
}

Option B: Make the Existing Check Effective

Change line 719 from:

if (max_message_size_ != MESSAGE_SIZE_UNLIMITED && current_message_size > max_message_size_)

to:

if (current_message_size > max_message_size_)

And set a reasonable default for max_message_size_ instead of MESSAGE_SIZE_UNLIMITED.

Option C: Incremental Allocation with Backpressure

Instead of allocating the full claimed size upfront, allocate in chunks and only grow as actual data arrives:

constexpr size_t MAX_RECV_BUFFER_GROW = 64 * 1024; // grow 64 KB at a time
if (missing_capacity_ > MAX_RECV_BUFFER_GROW) {
    missing_capacity_ = MAX_RECV_BUFFER_GROW;
}

Reproduction Steps

No response

Expected behaviour

No response

Logs and Screenshots

Image

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions