From f6ff1399147d2e865fe2bca53e2eb8514ca48804 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 9 Jun 2026 19:58:22 +0000 Subject: [PATCH 1/9] Initial plan From 13b099e9ee108383936a737bcf76561b0dfcb8a4 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 9 Jun 2026 20:03:30 +0000 Subject: [PATCH 2/9] Gate forwarded commands behind part-of-network state check --- CHANGELOG.md | 6 ++++ src/node/node_state.h | 64 ++++++++++++++++++++----------------------- 2 files changed, 35 insertions(+), 35 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index febfb4931044..17898f5b95f6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). +## [Unreleased] + +### Fixed + +- Forwarded commands are no longer processed until the node is part of the network, matching the existing behaviour for other node-to-node messages. Previously a forwarded command could be executed while the node was in an earlier startup state, which could lead to undefined behaviour for some commands (#7936). + ## [7.0.5] [7.0.5]: https://github.com/microsoft/CCF/releases/tag/ccf-7.0.5 diff --git a/src/node/node_state.h b/src/node/node_state.h index 23a316e7e490..59cfacfe2c88 100644 --- a/src/node/node_state.h +++ b/src/node/node_state.h @@ -2260,49 +2260,43 @@ namespace ccf const auto* payload_data = payload.data; auto payload_size = payload.size; - if (msg_type == NodeMsgType::forwarded_msg) + // Only process messages once part of network. This includes forwarded + // commands, which must not be executed until the node is part of the + // network, as some commands may otherwise exhibit undefined behaviour. + if ( + !sm.check(NodeStartupState::partOfNetwork) && + !sm.check(NodeStartupState::partOfPublicNetwork) && + !sm.check(NodeStartupState::readingPrivateLedger)) { - cmd_forwarder->recv_message(from, payload_data, payload_size); + LOG_DEBUG_FMT( + "Ignoring node msg received too early - current state is {}", + sm.value()); + return; } - else + + switch (msg_type) { - // Only process messages once part of network - if ( - !sm.check(NodeStartupState::partOfNetwork) && - !sm.check(NodeStartupState::partOfPublicNetwork) && - !sm.check(NodeStartupState::readingPrivateLedger)) + case forwarded_msg: { - LOG_DEBUG_FMT( - "Ignoring node msg received too early - current state is {}", - sm.value()); + cmd_forwarder->recv_message(from, payload_data, payload_size); return; } - - switch (msg_type) + case channel_msg: { - case forwarded_msg: - { - LOG_FAIL_FMT("Unexpected forwarded_msg in recv_node_inbound"); - return; - } - case channel_msg: - { - n2n_channels->recv_channel_message( - from, payload_data, payload_size); - return; - } + n2n_channels->recv_channel_message(from, payload_data, payload_size); + return; + } - case consensus_msg: - { - consensus->recv_message(from, payload_data, payload_size); - return; - } - default: - { - throw std::logic_error(fmt::format( - "Unknown node message type: {}", - static_cast(msg_type))); - } + case consensus_msg: + { + consensus->recv_message(from, payload_data, payload_size); + return; + } + default: + { + throw std::logic_error(fmt::format( + "Unknown node message type: {}", + static_cast(msg_type))); } } } From 4d13d36807cf9d3aac9f6567e1ecf218b943a9f8 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 10 Jun 2026 06:57:12 +0000 Subject: [PATCH 3/9] Add check_one_of() to StateMachine with unit test --- CMakeLists.txt | 5 ++ src/ds/state_machine.h | 6 ++ src/ds/test/state_machine.cpp | 101 ++++++++++++++++++++++++++++++++++ src/node/node_state.h | 8 +-- 4 files changed, 116 insertions(+), 4 deletions(-) create mode 100644 src/ds/test/state_machine.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index d437381db79f..0e8ca79698db 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -591,6 +591,11 @@ if(BUILD_TESTS) ${CMAKE_CURRENT_SOURCE_DIR}/src/ds/test/parse_json_safe.cpp ) + add_unit_test( + state_machine_test + ${CMAKE_CURRENT_SOURCE_DIR}/src/ds/test/state_machine.cpp + ) + add_unit_test( logger_test ${CMAKE_CURRENT_SOURCE_DIR}/src/ds/test/logger.cpp diff --git a/src/ds/state_machine.h b/src/ds/state_machine.h index 68c2328fc1cf..3f1d5bc6b944 100644 --- a/src/ds/state_machine.h +++ b/src/ds/state_machine.h @@ -5,6 +5,7 @@ #include "ds/internal_logger.h" #include +#include #include #include @@ -37,6 +38,11 @@ namespace ds return state_ == state.load(); } + [[nodiscard]] bool check_one_of(const std::set& states) const + { + return states.contains(state.load()); + } + [[nodiscard]] T value() const { return state.load(); diff --git a/src/ds/test/state_machine.cpp b/src/ds/test/state_machine.cpp new file mode 100644 index 000000000000..6ff0adb3c63e --- /dev/null +++ b/src/ds/test/state_machine.cpp @@ -0,0 +1,101 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the Apache 2.0 License. + +#include "../state_machine.h" + +#include + +namespace +{ + enum class Example + { + A, + B, + C, + D + }; +} + +template <> +struct fmt::formatter : fmt::formatter +{ + template + auto format(Example e, FormatContext& ctx) const + { + std::string_view name = "unknown"; + switch (e) + { + case Example::A: + name = "A"; + break; + case Example::B: + name = "B"; + break; + case Example::C: + name = "C"; + break; + case Example::D: + name = "D"; + break; + } + return fmt::formatter::format(name, ctx); + } +}; + +TEST_CASE("Basic state machine" * doctest::test_suite("state_machine")) +{ + ds::StateMachine sm("example", Example::A); + + REQUIRE(sm.value() == Example::A); + REQUIRE(sm.check(Example::A)); + REQUIRE_FALSE(sm.check(Example::B)); + REQUIRE_NOTHROW(sm.expect(Example::A)); + REQUIRE_THROWS_AS(sm.expect(Example::B), std::logic_error); + + sm.advance(Example::B); + + REQUIRE(sm.value() == Example::B); + REQUIRE(sm.check(Example::B)); + REQUIRE_FALSE(sm.check(Example::A)); + REQUIRE_NOTHROW(sm.expect(Example::B)); + REQUIRE_THROWS_AS(sm.expect(Example::A), std::logic_error); +} + +TEST_CASE("check_one_of" * doctest::test_suite("state_machine")) +{ + ds::StateMachine sm("example", Example::A); + + { + INFO("Current state is part of the set"); + REQUIRE(sm.check_one_of({Example::A})); + REQUIRE(sm.check_one_of({Example::A, Example::B})); + REQUIRE(sm.check_one_of({Example::A, Example::B, Example::C})); + } + + { + INFO("Current state is not part of the set"); + REQUIRE_FALSE(sm.check_one_of({Example::B})); + REQUIRE_FALSE(sm.check_one_of({Example::B, Example::C})); + REQUIRE_FALSE(sm.check_one_of({Example::B, Example::C, Example::D})); + } + + { + INFO("Empty set never matches"); + REQUIRE_FALSE(sm.check_one_of({})); + } + + { + INFO("Set membership follows state transitions"); + const std::set states{Example::B, Example::C}; + REQUIRE_FALSE(sm.check_one_of(states)); + + sm.advance(Example::B); + REQUIRE(sm.check_one_of(states)); + + sm.advance(Example::C); + REQUIRE(sm.check_one_of(states)); + + sm.advance(Example::D); + REQUIRE_FALSE(sm.check_one_of(states)); + } +} diff --git a/src/node/node_state.h b/src/node/node_state.h index 59cfacfe2c88..d2ba2166282d 100644 --- a/src/node/node_state.h +++ b/src/node/node_state.h @@ -2263,10 +2263,10 @@ namespace ccf // Only process messages once part of network. This includes forwarded // commands, which must not be executed until the node is part of the // network, as some commands may otherwise exhibit undefined behaviour. - if ( - !sm.check(NodeStartupState::partOfNetwork) && - !sm.check(NodeStartupState::partOfPublicNetwork) && - !sm.check(NodeStartupState::readingPrivateLedger)) + if (!sm.check_one_of( + {NodeStartupState::partOfNetwork, + NodeStartupState::partOfPublicNetwork, + NodeStartupState::readingPrivateLedger})) { LOG_DEBUG_FMT( "Ignoring node msg received too early - current state is {}", From 82c02b083d3246f54c50b14d955220856fb6d430 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 10 Jun 2026 07:37:43 +0000 Subject: [PATCH 4/9] Add recv_node_inbound_message unit test; fix doctest main linking and formatting --- CMakeLists.txt | 5 + src/ds/test/state_machine.cpp | 1 + src/node/node_inbound_message.h | 68 ++++++++ src/node/node_state.h | 48 +----- src/node/test/node_inbound_message.cpp | 225 +++++++++++++++++++++++++ 5 files changed, 302 insertions(+), 45 deletions(-) create mode 100644 src/node/node_inbound_message.h create mode 100644 src/node/test/node_inbound_message.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 0e8ca79698db..9f6482570118 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -785,6 +785,11 @@ if(BUILD_TESTS) PRIVATE ccf_kv ccf_endpoints ccf_tasks ) + add_unit_test( + node_inbound_message_test + ${CMAKE_CURRENT_SOURCE_DIR}/src/node/test/node_inbound_message.cpp + ) + add_unit_test( indexing_test ${CMAKE_CURRENT_SOURCE_DIR}/src/indexing/test/indexing.cpp diff --git a/src/ds/test/state_machine.cpp b/src/ds/test/state_machine.cpp index 6ff0adb3c63e..2426e219233e 100644 --- a/src/ds/test/state_machine.cpp +++ b/src/ds/test/state_machine.cpp @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the Apache 2.0 License. +#define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN #include "../state_machine.h" #include diff --git a/src/node/node_inbound_message.h b/src/node/node_inbound_message.h new file mode 100644 index 000000000000..af73430a1f21 --- /dev/null +++ b/src/node/node_inbound_message.h @@ -0,0 +1,68 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the Apache 2.0 License. +#pragma once + +#include "ccf/entity_id.h" +#include "ccf/node_startup_state.h" +#include "ds/internal_logger.h" +#include "ds/state_machine.h" +#include "node/node_types.h" + +namespace ccf +{ + // Reads a serialised node_inbound message and dispatches it to the + // appropriate handler, but only once the node is part of the network. This + // includes forwarded commands, which must not be executed until the node is + // part of the network, as some commands may otherwise exhibit undefined + // behaviour. + template + void recv_node_inbound_message( + const uint8_t* data, + size_t size, + ::ds::StateMachine& sm, + TForwarder& cmd_forwarder, + TChannels& n2n_channels, + TConsensus& consensus) + { + auto [msg_type, from, payload] = + ringbuffer::read_message(data, size); + + const auto* payload_data = payload.data; + auto payload_size = payload.size; + + if (!sm.check_one_of( + {NodeStartupState::partOfNetwork, + NodeStartupState::partOfPublicNetwork, + NodeStartupState::readingPrivateLedger})) + { + LOG_DEBUG_FMT( + "Ignoring node msg received too early - current state is {}", + sm.value()); + return; + } + + switch (msg_type) + { + case forwarded_msg: + { + cmd_forwarder.recv_message(from, payload_data, payload_size); + return; + } + case channel_msg: + { + n2n_channels.recv_channel_message(from, payload_data, payload_size); + return; + } + case consensus_msg: + { + consensus.recv_message(from, payload_data, payload_size); + return; + } + default: + { + throw std::logic_error(fmt::format( + "Unknown node message type: {}", static_cast(msg_type))); + } + } + } +} diff --git a/src/node/node_state.h b/src/node/node_state.h index d2ba2166282d..9d180cf16eaa 100644 --- a/src/node/node_state.h +++ b/src/node/node_state.h @@ -41,6 +41,7 @@ #include "node/ledger_secret.h" #include "node/ledger_secrets.h" #include "node/local_sealing.h" +#include "node/node_inbound_message.h" #include "node/node_to_node_channel_manager.h" #include "node/recovery_decision_protocol.h" #include "node/signature_cache_subsystem.h" @@ -2254,51 +2255,8 @@ namespace ccf void recv_node_inbound(const uint8_t* data, size_t size) { - auto [msg_type, from, payload] = - ringbuffer::read_message(data, size); - - const auto* payload_data = payload.data; - auto payload_size = payload.size; - - // Only process messages once part of network. This includes forwarded - // commands, which must not be executed until the node is part of the - // network, as some commands may otherwise exhibit undefined behaviour. - if (!sm.check_one_of( - {NodeStartupState::partOfNetwork, - NodeStartupState::partOfPublicNetwork, - NodeStartupState::readingPrivateLedger})) - { - LOG_DEBUG_FMT( - "Ignoring node msg received too early - current state is {}", - sm.value()); - return; - } - - switch (msg_type) - { - case forwarded_msg: - { - cmd_forwarder->recv_message(from, payload_data, payload_size); - return; - } - case channel_msg: - { - n2n_channels->recv_channel_message(from, payload_data, payload_size); - return; - } - - case consensus_msg: - { - consensus->recv_message(from, payload_data, payload_size); - return; - } - default: - { - throw std::logic_error(fmt::format( - "Unknown node message type: {}", - static_cast(msg_type))); - } - } + recv_node_inbound_message( + data, size, sm, *cmd_forwarder, *n2n_channels, *consensus); } // diff --git a/src/node/test/node_inbound_message.cpp b/src/node/test/node_inbound_message.cpp new file mode 100644 index 000000000000..453065c9963a --- /dev/null +++ b/src/node/test/node_inbound_message.cpp @@ -0,0 +1,225 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the Apache 2.0 License. + +#define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN +#include "../node_inbound_message.h" + +#include "ccf/ds/nonstd.h" + +#include +#include +#include + +namespace +{ + // Records the most recent message passed to it, so a test can assert whether + // (and with what arguments) a particular handler was invoked. + struct StubHandler + { + std::optional last_from; + std::vector last_payload; + size_t call_count = 0; + + void recv_message(const ccf::NodeId& from, const uint8_t* data, size_t size) + { + last_from = from; + last_payload.assign(data, data + size); + ++call_count; + } + + void recv_channel_message( + const ccf::NodeId& from, const uint8_t* data, size_t size) + { + recv_message(from, data, size); + } + }; + + std::vector serialise_node_inbound( + ccf::NodeMsgType msg_type, + const ccf::NodeId& from, + const std::vector& payload) + { + auto sections = + ringbuffer::MessageSerializers::serialize( + msg_type, + from.value(), + serializer::ByteRange{payload.data(), payload.size()}); + + std::vector result; + ccf::nonstd::tuple_for_each(sections, [&](const auto& s) { + result.insert(result.end(), s->data(), s->data() + s->size()); + }); + return result; + } +} + +TEST_CASE( + "recv_node_inbound_message gating" * + doctest::test_suite("node_inbound_message")) +{ + const ccf::NodeId from("0123456789abcdef"); + const std::vector payload{1, 2, 3, 4, 5}; + + const auto early_states = { + ccf::NodeStartupState::uninitialized, + ccf::NodeStartupState::initialized, + ccf::NodeStartupState::pending, + ccf::NodeStartupState::readingPublicLedger}; + + const auto active_states = { + ccf::NodeStartupState::partOfNetwork, + ccf::NodeStartupState::partOfPublicNetwork, + ccf::NodeStartupState::readingPrivateLedger}; + + SUBCASE("Forwarded commands are not processed before part of network") + { + const auto serialised = + serialise_node_inbound(ccf::forwarded_msg, from, payload); + + for (const auto state : early_states) + { + INFO("Early state: ", state); + ds::StateMachine sm("test", state); + StubHandler forwarder; + StubHandler channels; + StubHandler consensus; + + ccf::recv_node_inbound_message( + serialised.data(), + serialised.size(), + sm, + forwarder, + channels, + consensus); + + REQUIRE(forwarder.call_count == 0); + REQUIRE(channels.call_count == 0); + REQUIRE(consensus.call_count == 0); + } + } + + SUBCASE("Forwarded commands are processed once part of network") + { + const auto serialised = + serialise_node_inbound(ccf::forwarded_msg, from, payload); + + for (const auto state : active_states) + { + INFO("Active state: ", state); + ds::StateMachine sm("test", state); + StubHandler forwarder; + StubHandler channels; + StubHandler consensus; + + ccf::recv_node_inbound_message( + serialised.data(), + serialised.size(), + sm, + forwarder, + channels, + consensus); + + REQUIRE(forwarder.call_count == 1); + REQUIRE(forwarder.last_from == from); + REQUIRE(forwarder.last_payload == payload); + REQUIRE(channels.call_count == 0); + REQUIRE(consensus.call_count == 0); + } + } + + SUBCASE("Channel messages are gated and dispatched identically") + { + const auto serialised = + serialise_node_inbound(ccf::channel_msg, from, payload); + + { + INFO("Dropped before part of network"); + ds::StateMachine sm( + "test", ccf::NodeStartupState::pending); + StubHandler forwarder; + StubHandler channels; + StubHandler consensus; + + ccf::recv_node_inbound_message( + serialised.data(), + serialised.size(), + sm, + forwarder, + channels, + consensus); + + REQUIRE(channels.call_count == 0); + } + + { + INFO("Dispatched once part of network"); + ds::StateMachine sm( + "test", ccf::NodeStartupState::partOfNetwork); + StubHandler forwarder; + StubHandler channels; + StubHandler consensus; + + ccf::recv_node_inbound_message( + serialised.data(), + serialised.size(), + sm, + forwarder, + channels, + consensus); + + REQUIRE(channels.call_count == 1); + REQUIRE(channels.last_from == from); + REQUIRE(channels.last_payload == payload); + REQUIRE(forwarder.call_count == 0); + REQUIRE(consensus.call_count == 0); + } + } + + SUBCASE("Consensus messages are gated and dispatched identically") + { + const auto serialised = + serialise_node_inbound(ccf::consensus_msg, from, payload); + + { + INFO("Dropped before part of network"); + ds::StateMachine sm( + "test", ccf::NodeStartupState::initialized); + StubHandler forwarder; + StubHandler channels; + StubHandler consensus; + + ccf::recv_node_inbound_message( + serialised.data(), + serialised.size(), + sm, + forwarder, + channels, + consensus); + + REQUIRE(consensus.call_count == 0); + } + + { + INFO("Dispatched once part of network"); + ds::StateMachine sm( + "test", ccf::NodeStartupState::readingPrivateLedger); + StubHandler forwarder; + StubHandler channels; + StubHandler consensus; + + ccf::recv_node_inbound_message( + serialised.data(), + serialised.size(), + sm, + forwarder, + channels, + consensus); + + REQUIRE(consensus.call_count == 1); + REQUIRE(consensus.last_from == from); + REQUIRE(consensus.last_payload == payload); + REQUIRE(forwarder.call_count == 0); + REQUIRE(channels.call_count == 0); + } + } +} From 84f1c429c8be9f41d3fcac2b0e679735b3b94185 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 10 Jun 2026 08:48:45 +0000 Subject: [PATCH 5/9] Fix CHANGELOG version and bump pyproject to satisfy release notes check --- CHANGELOG.md | 4 +++- python/pyproject.toml | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 17898f5b95f6..389bbee055a0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,9 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). -## [Unreleased] +## [7.0.6] + +[7.0.6]: https://github.com/microsoft/CCF/releases/tag/ccf-7.0.6 ### Fixed diff --git a/python/pyproject.toml b/python/pyproject.toml index e8809e787664..ebcac209cc44 100644 --- a/python/pyproject.toml +++ b/python/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "ccf" -version = "7.0.5" +version = "7.0.6" authors = [ { name="CCF Team", email="CCF-Sec@microsoft.com" }, ] From 1edc288971598eb27e03cf676bc30608604faf2d Mon Sep 17 00:00:00 2001 From: Amaury Chamayou Date: Wed, 10 Jun 2026 15:04:26 +0100 Subject: [PATCH 6/9] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- src/ds/state_machine.h | 1 + 1 file changed, 1 insertion(+) diff --git a/src/ds/state_machine.h b/src/ds/state_machine.h index 3f1d5bc6b944..0eb4c194f029 100644 --- a/src/ds/state_machine.h +++ b/src/ds/state_machine.h @@ -6,6 +6,7 @@ #include #include +#include #include #include From e7649d387080f58ec8cba022b21d5771132b55f8 Mon Sep 17 00:00:00 2001 From: Amaury Chamayou Date: Wed, 10 Jun 2026 15:06:27 +0100 Subject: [PATCH 7/9] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- src/node/node_inbound_message.h | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/node/node_inbound_message.h b/src/node/node_inbound_message.h index af73430a1f21..9c08911d54a4 100644 --- a/src/node/node_inbound_message.h +++ b/src/node/node_inbound_message.h @@ -30,10 +30,12 @@ namespace ccf const auto* payload_data = payload.data; auto payload_size = payload.size; - if (!sm.check_one_of( - {NodeStartupState::partOfNetwork, - NodeStartupState::partOfPublicNetwork, - NodeStartupState::readingPrivateLedger})) + static const std::set active_states{ + NodeStartupState::partOfNetwork, + NodeStartupState::partOfPublicNetwork, + NodeStartupState::readingPrivateLedger}; + + if (!sm.check_one_of(active_states)) { LOG_DEBUG_FMT( "Ignoring node msg received too early - current state is {}", From 0c88bb7786dcec8d61a3bf68657331f2144816a8 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 10 Jun 2026 17:54:23 +0000 Subject: [PATCH 8/9] Avoid early null deref in recv_node_inbound dispatch Co-authored-by: achamayou <4016369+achamayou@users.noreply.github.com> --- src/node/node_inbound_message.h | 12 ++++----- src/node/node_state.h | 7 ++++- src/node/test/node_inbound_message.cpp | 36 +++++++++++++------------- 3 files changed, 30 insertions(+), 25 deletions(-) diff --git a/src/node/node_inbound_message.h b/src/node/node_inbound_message.h index 9c08911d54a4..4ac3b201c68c 100644 --- a/src/node/node_inbound_message.h +++ b/src/node/node_inbound_message.h @@ -20,9 +20,9 @@ namespace ccf const uint8_t* data, size_t size, ::ds::StateMachine& sm, - TForwarder& cmd_forwarder, - TChannels& n2n_channels, - TConsensus& consensus) + TForwarder* cmd_forwarder, + TChannels* n2n_channels, + TConsensus* consensus) { auto [msg_type, from, payload] = ringbuffer::read_message(data, size); @@ -47,17 +47,17 @@ namespace ccf { case forwarded_msg: { - cmd_forwarder.recv_message(from, payload_data, payload_size); + cmd_forwarder->recv_message(from, payload_data, payload_size); return; } case channel_msg: { - n2n_channels.recv_channel_message(from, payload_data, payload_size); + n2n_channels->recv_channel_message(from, payload_data, payload_size); return; } case consensus_msg: { - consensus.recv_message(from, payload_data, payload_size); + consensus->recv_message(from, payload_data, payload_size); return; } default: diff --git a/src/node/node_state.h b/src/node/node_state.h index 9d180cf16eaa..53d158831b8d 100644 --- a/src/node/node_state.h +++ b/src/node/node_state.h @@ -2256,7 +2256,12 @@ namespace ccf void recv_node_inbound(const uint8_t* data, size_t size) { recv_node_inbound_message( - data, size, sm, *cmd_forwarder, *n2n_channels, *consensus); + data, + size, + sm, + cmd_forwarder.get(), + n2n_channels.get(), + consensus.get()); } // diff --git a/src/node/test/node_inbound_message.cpp b/src/node/test/node_inbound_message.cpp index 453065c9963a..8198d845fbec 100644 --- a/src/node/test/node_inbound_message.cpp +++ b/src/node/test/node_inbound_message.cpp @@ -88,9 +88,9 @@ TEST_CASE( serialised.data(), serialised.size(), sm, - forwarder, - channels, - consensus); + &forwarder, + &channels, + &consensus); REQUIRE(forwarder.call_count == 0); REQUIRE(channels.call_count == 0); @@ -115,9 +115,9 @@ TEST_CASE( serialised.data(), serialised.size(), sm, - forwarder, - channels, - consensus); + &forwarder, + &channels, + &consensus); REQUIRE(forwarder.call_count == 1); REQUIRE(forwarder.last_from == from); @@ -144,9 +144,9 @@ TEST_CASE( serialised.data(), serialised.size(), sm, - forwarder, - channels, - consensus); + &forwarder, + &channels, + &consensus); REQUIRE(channels.call_count == 0); } @@ -163,9 +163,9 @@ TEST_CASE( serialised.data(), serialised.size(), sm, - forwarder, - channels, - consensus); + &forwarder, + &channels, + &consensus); REQUIRE(channels.call_count == 1); REQUIRE(channels.last_from == from); @@ -192,9 +192,9 @@ TEST_CASE( serialised.data(), serialised.size(), sm, - forwarder, - channels, - consensus); + &forwarder, + &channels, + &consensus); REQUIRE(consensus.call_count == 0); } @@ -211,9 +211,9 @@ TEST_CASE( serialised.data(), serialised.size(), sm, - forwarder, - channels, - consensus); + &forwarder, + &channels, + &consensus); REQUIRE(consensus.call_count == 1); REQUIRE(consensus.last_from == from); From c77546a10b8eea54922e481673623393cfbe1dfe Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 10 Jun 2026 17:55:23 +0000 Subject: [PATCH 9/9] Guard nullable handlers in node inbound dispatch Co-authored-by: achamayou <4016369+achamayou@users.noreply.github.com> --- src/node/node_inbound_message.h | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/src/node/node_inbound_message.h b/src/node/node_inbound_message.h index 4ac3b201c68c..bc4278c39553 100644 --- a/src/node/node_inbound_message.h +++ b/src/node/node_inbound_message.h @@ -47,16 +47,36 @@ namespace ccf { case forwarded_msg: { + if (cmd_forwarder == nullptr) + { + LOG_FAIL_FMT( + "Ignoring forwarded node message: command forwarder not " + "initialised"); + return; + } cmd_forwarder->recv_message(from, payload_data, payload_size); return; } case channel_msg: { + if (n2n_channels == nullptr) + { + LOG_FAIL_FMT( + "Ignoring channel node message: node-to-node channels not " + "initialised"); + return; + } n2n_channels->recv_channel_message(from, payload_data, payload_size); return; } case consensus_msg: { + if (consensus == nullptr) + { + LOG_FAIL_FMT( + "Ignoring consensus node message: consensus not initialised"); + return; + } consensus->recv_message(from, payload_data, payload_size); return; }