epee: validate Levin response command before buffering
What changed, and why it matters
This commit adds a safety check in Monero's network code that verifies an incoming response matches the command that was originally requested before the response is accepted and buffered. Without this check, a malicious or misbehaving peer could potentially send an unexpected response that the software would process incorrectly, possibly causing confusion, resource waste, or protocol errors. The patch is defensive and partial—it validates the command but does not by itself fix a fully demonstrated exploit.
Treat as a security hardening fix and include in the next release. Review whether additional response fields (e.g., request/response IDs, protocol version) should also be validated, and assess whether the pending-handler queue can be manipulated by an attacker to bypass the check.
Security signals we found
Network protocol input validation added
Response command mismatch now rejected before buffering
Error logging added for unexpected levin responses
Defensive hardening of P2P message handling
Evidence from the diff
The change introduces a command() accessor on invoke_response_handler_base and a validate_response_command() helper in the Levin async protocol handler. Before buffering/processing a Levin response body, the handler now checks that m_current_head.m_command equals the command stored in the front pending invoke response handler. A mismatch logs an error and aborts processing. This prevents a peer from injecting a response for a different command than the one the local node is waiting on. The patch is partial: it validates command identity but does not show the complete attack chain or additional hardening.
Changed components
contrib/epee/include/net/levin_protocol_handler_async.hMonero P2P Levin protocol handlerInspect captured patch +36 / −1
diff --git a/contrib/epee/include/net/levin_protocol_handler_async.h b/contrib/epee/include/net/levin_protocol_handler_async.h
index 4a0e565..4dc68af 100644
--- a/contrib/epee/include/net/levin_protocol_handler_async.h
+++ b/contrib/epee/include/net/levin_protocol_handler_async.h
@@ -193,6 +193,7 @@ public:
struct invoke_response_handler_base
{
virtual ~invoke_response_handler_base() {}
+ virtual int command() const noexcept=0;
virtual bool handle(int res, const epee::span<const uint8_t> buff, connection_context& context)=0;
virtual void cancel()=0;
virtual bool cancel_timer()=0;
@@ -241,6 +242,11 @@ public:
{
failure(LEVIN_ERROR_CONNECTION_DESTROYED);
}
+
+ virtual int command() const noexcept override final
+ {
+ return m_command;
+ }
virtual bool handle(int res, const epee::span<const uint8_t> buff, typename async_protocol_handler::connection_context& context) override final
{
@@ -285,6 +291,29 @@ public:
};
critical_section m_invoke_response_handlers_lock;
std::list<std::weak_ptr<invoke_response_handler_base>> m_invoke_response_handlers;
+
+ bool validate_response_command()
+ {
+ if (m_oponent_protocol_ver != LEVIN_PROTOCOL_VER_1 || !(m_current_head.m_flags & LEVIN_PACKET_RESPONSE))
+ return true;
+
+ CRITICAL_REGION_LOCAL(m_invoke_response_handlers_lock);
+ if (m_invoke_response_handlers.empty())
+ {
+ MERROR(m_connection_context << "Received levin response but have no invoke handlers");
+ return false;
+ }
+
+ const std::shared_ptr<invoke_response_handler_base> response_handler = m_invoke_response_handlers.front().lock();
+ if (!response_handler || m_current_head.m_command != static_cast<uint32_t>(response_handler->command()))
+ {
+ MERROR(m_connection_context << "Received levin response command " << m_current_head.m_command
+ << " while waiting for " << (response_handler ? response_handler->command() : -1));
+ return false;
+ }
+
+ return true;
+ }
template<class callback_t>
bool add_invoke_response_handler(const callback_t &cb, const std::chrono::milliseconds timeout, std::shared_ptr<net_utils::service_endpoint<derived_handler>> con, int command)
@@ -466,6 +495,9 @@ public:
buff_to_invoke = {buff_to_invoke.data(), std::size_t(inner_size)};
+ if (!validate_response_command())
+ return false;
+
const size_t max_bytes = m_connection_context.get_max_bytes(m_current_head.m_command);
if(buff_to_invoke.size() > std::min<size_t>(max_packet_size, max_bytes))
{
@@ -571,9 +603,12 @@ public:
return false;
}
+ m_oponent_protocol_ver = m_current_head.m_protocol_version;
+ if (!validate_response_command())
+ return false;
+
m_cache_in_buffer.erase(sizeof(bucket_head2));
m_state = stream_state_body;
- m_oponent_protocol_ver = m_current_head.m_protocol_version;
const size_t max_bytes = m_connection_context.get_max_bytes(m_current_head.m_command);
if(m_current_head.m_cb > std::min<size_t>(max_packet_size, max_bytes))
{
Why this scored 57/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.