From 5ab22bb5d9050badb0886983ba9b2678c9be9b6b Mon Sep 17 00:00:00 2001 From: Anatolii Smolianinov Date: Thu, 25 Apr 2024 16:28:53 +0200 Subject: [PATCH 1/3] Sei remote sign hotfix: aggregate tcp chunks before unmarshal proto ```go // tendermint/cometbft proposal: type Proposal struct { Type SignedMsgType Height int64 Round int32 PolRound int32 BlockID BlockID Timestamp time.Time Signature []byte } ``` ```go // vs sei-tendermint proposal type Proposal struct { Type SignedMsgType Height int64 Round int32 PolRound int32 BlockID BlockID Timestamp time.Time Signature []byte // this is a list, and can be very long... TxKeys []*TxKey Evidence *EvidenceList LastCommit *Commit Header Header ProposerAddress []byte } ``` Since Proposal has TxKeys and other lists, Proposal has variable length It is easily goes > 1024 bytes if block has big mount of txs. And it is not a problem of canonical tendermint/cometbft implementations since due to its message structure, it has a fixed max length < 1024 (DATA_MAX_SIZE) sei-tendermint, when it connects to remote signer over tcp, sends proposal divided by chunk of DATA_MAX_SIZE (1024) each, which kind of fits the expectation of tmkms. However, tmkms never tries to aggregate chunks. In fact, it is impossible for tmkms to implement aggregation properly without knowing the length beforehand: which is not provided by tendermint protocol. There might be a confusioon also, because all implementations of tendermint send lenght-delimited protobufs, and tmkms also reads with a function "length delimited". However, it actually means that the protobuf msg is prepended by it's length: so that when tmkms reads 1024 bytes it knows which zeroes are payload and which a need to be cut. Another words, it has nothing to do with multi-chunk payload. Which means that sei-tendermint just doesn't bother about tcp remote signer, and it is impossible to make it work with tmkms without rewriting both and adding this custom protocol of "aggregate chunks until you get full message length". -- This code implements aggregation by trying to unmarshal aggregated message each time it gets a new chunk. I don't think it is a good idea in a long run, however, the alternative would be to adjust both Sei and tmkms, rolling out new length-aware protocol between them -- I'm not sure how sufficient it is and definitely needs a discussion. Current solution is compartable with both cometbft/tendermint and sei-tendermint, however, way less efficient then the original `read` implementation of tmkms. P.S: Apart from custom length-aware protocol, there is another option: implement grpc in tmkms, which seem to be supported by sei-tendermint. --- src/rpc.rs | 41 +++++++++++++++++++++++++++++++---------- 1 file changed, 31 insertions(+), 10 deletions(-) diff --git a/src/rpc.rs b/src/rpc.rs index 428669ff..7e2a9819 100644 --- a/src/rpc.rs +++ b/src/rpc.rs @@ -7,12 +7,9 @@ use crate::privval::SignableMsg; use prost::Message as _; use std::io::Read; use tendermint::{chain, Proposal, Vote}; +use tendermint_p2p::secret_connection::DATA_MAX_SIZE; use tendermint_proto as proto; -// TODO(tarcieri): use `tendermint_p2p::secret_connection::DATA_MAX_SIZE` -// See informalsystems/tendermint-rs#1356 -const DATA_MAX_SIZE: usize = 262144; - use crate::{ error::{Error, ErrorKind}, prelude::*, @@ -31,12 +28,36 @@ pub enum Request { impl Request { /// Read a request from the given readable. pub fn read(conn: &mut impl Read, expected_chain_id: &chain::Id) -> Result { - let msg_bytes = read_msg(conn)?; - - // Parse Protobuf-encoded request message - let msg = proto::privval::Message::decode_length_delimited(msg_bytes.as_ref()) - .map_err(|e| format_err!(ErrorKind::ProtocolError, "malformed message packet: {}", e))? - .sum; + let mut msg_bytes: Vec = vec![]; + let msg; + + // fix for Sei: collect incoming bytes of Protobuf from incoming msg + loop { + let mut msg_chunk = read_msg(conn)?; + let chunk_len = msg_chunk.len(); + msg_bytes.append(&mut msg_chunk); + + // if we can decode it, great, break the loop + match proto::privval::Message::decode_length_delimited(msg_bytes.as_ref()) { + Ok(m) => { + msg = m.sum; + break; + } + Err(e) => { + // if chunk_len < DATA_MAX_SIZE (1024) we assume it was the end of the message and it is malformed + if chunk_len < DATA_MAX_SIZE { + return Err(format_err!( + ErrorKind::ProtocolError, + "malformed message packet: {}", + e + ) + .into()); + } + // otherwise, we go to start of the loop assuming next chunk(s) + // will fill the message + } + } + } let (req, chain_id) = match msg { Some(proto::privval::message::Sum::SignVoteRequest( From 39b4013bf6feb3dc71eadc9400bbae3903440f8a Mon Sep 17 00:00:00 2001 From: Anatolii Smolianinov Date: Thu, 18 Jul 2024 10:46:22 +0200 Subject: [PATCH 2/3] integration test with bigger Proposal --- tests/integration.rs | 24 +++++++++++++++++++++ tests/support/buffer-overflow-proposal.bin | Bin 0 -> 1894 bytes 2 files changed, 24 insertions(+) create mode 100644 tests/support/buffer-overflow-proposal.bin diff --git a/tests/integration.rs b/tests/integration.rs index 5d24a4d4..eda8b7f7 100644 --- a/tests/integration.rs +++ b/tests/integration.rs @@ -5,6 +5,7 @@ use chrono::{DateTime, Utc}; use prost::Message; use rand::Rng; use signature::Verifier; +use std::fs::File; use std::{ fs, io::{self, Cursor, Read, Write}, @@ -617,6 +618,20 @@ fn test_handle_and_sign_ping_pong() { }); } +#[test] +fn test_buffer_underflow_sign_proposal() { + let key_type = KeyType::Consensus; + ProtocolTester::apply(&key_type, |mut pt| { + send_buffer_underflow_request(&mut pt); + let response: Result<(), ()> = match read_response(&mut pt) { + proto::privval::message::Sum::SignedProposalResponse(_) => Ok(()), + other => panic!("unexpected message type in response: {other:?}"), + }; + + assert!(response.is_ok()); + }); +} + /// Encode request as a Protobuf message fn send_request(request: proto::privval::message::Sum, pt: &mut ProtocolTester) { let mut buf = vec![]; @@ -627,6 +642,15 @@ fn send_request(request: proto::privval::message::Sum, pt: &mut ProtocolTester) pt.write_all(&buf).unwrap(); } +/// Opens a binary file with big proposal (> 1024 bytes, from Sei network) +/// and sends via protocol tester +fn send_buffer_underflow_request(pt: &mut ProtocolTester) { + let mut file = File::open("tests/support/buffer-underflow-proposal.bin").unwrap(); + let mut buf = Vec::::new(); + file.read_to_end(&mut buf).unwrap(); + pt.write_all(&buf).unwrap(); +} + /// Read the response as a Protobuf message fn read_response(pt: &mut ProtocolTester) -> proto::privval::message::Sum { let mut resp_buf = vec![0u8; 4096]; diff --git a/tests/support/buffer-overflow-proposal.bin b/tests/support/buffer-overflow-proposal.bin new file mode 100644 index 0000000000000000000000000000000000000000..5cbcabc6430c845cd425118c7380879f088dbf72 GIT binary patch literal 1894 zcmb`{cTm#_7Qk`h3dGPth{_U>Byc?se(Z;B=cKl z>KN|vqQIRMb>laA$ezK7DemDwP3kCWqz5{!swV+vlq|d!gN~;Qe6VN;GH%iS`U8Yo z4P9C*N)l&5N|w|2A_+a&oKFUx*|8r!14cu+4CXu1VtN0FTCssi-`e(rWTJZSkdkQz z+!E#}U;t)O>8hrm4or`T04w56aFUjvLzckM8|Ffh+1*;v+xdVg1fj}n*8k?D*;P7e z^Kh%N!n5<$sYITV@tZNTDsK5%z)bYz*A_2HhGxyQRx?=hGFCQgl;NGt(}(MY!JSN2 z5@4=1vjQd}Sy=&yvX7p7jYHN)^V6UfVj@kTR;&~EX*6K$E`U4z_wJ)6nl=dzj?J_B z2a+EB<7Dwo->)rQ=}!#-gBNS5zlGtOzkJaN=gIbbe2;5T+EKeyn2WC2=KMk70tP#p zN;jb9O6Q%oQi!`t<4xed*mtkowYa*G^|Nv^e;qJQYWmmfc}fMZyXfRu#eMh-_kpHK z{>>+k^*6|YirbEW$$h}iV7jnNxY-{23Ha*@5vr{7Rkcx6=aPQvmw}G zXQ$nnRo6|xY-?Fe9@o|te5Zx@v@ld-vn2Lw1ry%Mh+DQMA@8v=cGZ_~fv;;i6U(+T3m)Q5_U&?$8(!Ikk)e%xRp?`}ydezVY@+Ud3_O z7NguspXF||js6hVtlZ*rB*1uk(aPft&Eg??%ukwT^0;W1;o}=pJgNDeC1ZVs$Pa+w z=S$z1F*{qQE&ruJ)TCCKW#Y(NPJpi&*V>Ow5M7vnQA)qZDt#B$j>gjrW|MBjv=WDO zLfS|~m4hS38N>0ZfEizjvQ$j$zM5f+d~=MU69?V3Yu4uW`tKvXGt0_sp@4B{5Y5&x z^Y_>d42hw@`Jwpwm}s{~k3z>l(3aDL@-Kifz&qhA&GpUhMvNFm7=L#M&+Q;4a>||ohRmNYU$S{Px}Kiz z>3_Uv4Hog^Z{w^{rN_;Klw)m0&VWIp!0ogg!cBL|-{pp^stu_yej&$Gd|ay|IEd|k z`A5LGc)EJ)3Q?w?v0ouDJSg7N0F5_V{fT?omXM$qPZ|J>u#a0c_`7}2dC+N2zxhmz zAd|9-;s}@@zu(a0Q1P* z`wYnr_Z3s3=g=IMQyn>0`ug^X=g4b*4AG@~vp)fbG0KT;X`imJOsN#hen)CpoRUY- zp3R+iF!m_igld>vY8ZTkFc+l&Lgflf$IrDRcn%5DadtK3q;PsqE<+ zG>8!E@F&atP+G4b3GV)9_}ZS#PJe?oKPn=evZBe@(U(sN7}=q*Y6)}V5|y`u`q!VG z91M-;N4GxEnOxUZ_r6tB>~(IlfaE!4ciCP|O#Qy@o{UV6xCiwc$RV%_So^$mCvUZlGD48avSw@SXlo zbqsQUz`?B$L^Hy?N2MvB6(x_DySTU{_Ob7!V+IxP$bH2kGga^%uFLuDUm&_3w@B}% z$vG)hlM4i$U#hK-Ea-AGt^cQRLRw2pF)tpD8H`rD9UHVWCqZj=J{Dyq(~+G*+-7P} zl{}E)PfZGA2r9N`CdZY~WIGBnUKix{vgHz06^$#D z*;4ubii(e`(RU$OG~qzXArQIH(1Xq1Q-tU4?)2q^wy0{#_S literal 0 HcmV?d00001 From bf95b8fbdf5ae2b16b2e71bdfcab3b61414bdf63 Mon Sep 17 00:00:00 2001 From: Mateusz Kaczanowski Date: Wed, 28 Aug 2024 10:20:02 +0200 Subject: [PATCH 3/3] rename buffer-overflow-proposal.bin to buffer-underflow-proposal.bin --- ...w-proposal.bin => buffer-underflow-proposal.bin} | Bin 1 file changed, 0 insertions(+), 0 deletions(-) rename tests/support/{buffer-overflow-proposal.bin => buffer-underflow-proposal.bin} (100%) diff --git a/tests/support/buffer-overflow-proposal.bin b/tests/support/buffer-underflow-proposal.bin similarity index 100% rename from tests/support/buffer-overflow-proposal.bin rename to tests/support/buffer-underflow-proposal.bin