Skip to content

Commit 1b6c463

Browse files
committed
Merge bitcoin#21407: i2p: limit the size of incoming messages
7059e6d test: add a test to ensure RecvUntilTerminator() limit works (Vasil Dimov) 80a5a8e i2p: limit the size of incoming messages (Vasil Dimov) Pull request description: Put a limit on the amount of data `Sock::RecvUntilTerminator()` can read if no terminator is received. In the case of I2P this avoids a runaway (or malicious) I2P proxy sending us tons of data without a terminator before a timeout is triggered. ACKs for top commit: laanwj: Re-ACK 7059e6d Tree-SHA512: 21f3f3468c765c726cdc877eae847cdb4dbe225d94c5bd1849bd752c9761fac881c554f16ea7a685ad40312159d554e126c481e21c5fd83a6d947060b920373d
2 parents 7723479 + 7059e6d commit 1b6c463

File tree

5 files changed

+53
-5
lines changed

5 files changed

+53
-5
lines changed

src/i2p.cpp

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -153,7 +153,7 @@ bool Session::Accept(Connection& conn)
153153
}
154154

155155
const std::string& peer_dest =
156-
conn.sock.RecvUntilTerminator('\n', MAX_WAIT_FOR_IO, *m_interrupt);
156+
conn.sock.RecvUntilTerminator('\n', MAX_WAIT_FOR_IO, *m_interrupt, MAX_MSG_SIZE);
157157

158158
conn.peer = CService(DestB64ToAddr(peer_dest), Params().GetDefaultPort());
159159

@@ -252,7 +252,7 @@ Session::Reply Session::SendRequestAndGetReply(const Sock& sock,
252252
// signaled.
253253
static constexpr auto recv_timeout = 3min;
254254

255-
reply.full = sock.RecvUntilTerminator('\n', recv_timeout, *m_interrupt);
255+
reply.full = sock.RecvUntilTerminator('\n', recv_timeout, *m_interrupt, MAX_MSG_SIZE);
256256

257257
for (const auto& kv : spanparsing::Split(reply.full, ' ')) {
258258
const auto& pos = std::find(kv.begin(), kv.end(), '=');

src/i2p.h

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,14 @@ struct Connection {
4040

4141
namespace sam {
4242

43+
/**
44+
* The maximum size of an incoming message from the I2P SAM proxy (in bytes).
45+
* Used to avoid a runaway proxy from sending us an "unlimited" amount of data without a terminator.
46+
* The longest known message is ~1400 bytes, so this is high enough not to be triggered during
47+
* normal operation, yet low enough to avoid a malicious proxy from filling our memory.
48+
*/
49+
static constexpr size_t MAX_MSG_SIZE{65536};
50+
4351
/**
4452
* I2P SAM session.
4553
*/

src/test/sock_tests.cpp

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,13 @@
44

55
#include <compat.h>
66
#include <test/util/setup_common.h>
7+
#include <threadinterrupt.h>
78
#include <util/sock.h>
89
#include <util/system.h>
910

1011
#include <boost/test/unit_test.hpp>
1112

13+
#include <cassert>
1214
#include <thread>
1315

1416
using namespace std::chrono_literals;
@@ -144,6 +146,35 @@ BOOST_AUTO_TEST_CASE(wait)
144146
waiter.join();
145147
}
146148

149+
BOOST_AUTO_TEST_CASE(recv_until_terminator_limit)
150+
{
151+
constexpr auto timeout = 1min; // High enough so that it is never hit.
152+
CThreadInterrupt interrupt;
153+
int s[2];
154+
CreateSocketPair(s);
155+
156+
Sock sock_send(s[0]);
157+
Sock sock_recv(s[1]);
158+
159+
std::thread receiver([&sock_recv, &timeout, &interrupt]() {
160+
constexpr size_t max_data{10};
161+
bool threw_as_expected{false};
162+
// BOOST_CHECK_EXCEPTION() writes to some variables shared with the main thread which
163+
// creates a data race. So mimic it manually.
164+
try {
165+
sock_recv.RecvUntilTerminator('\n', timeout, interrupt, max_data);
166+
} catch (const std::runtime_error& e) {
167+
threw_as_expected = HasReason("too many bytes without a terminator")(e);
168+
}
169+
assert(threw_as_expected);
170+
});
171+
172+
BOOST_REQUIRE_NO_THROW(sock_send.SendComplete("1234567", timeout, interrupt));
173+
BOOST_REQUIRE_NO_THROW(sock_send.SendComplete("89a\n", timeout, interrupt));
174+
175+
receiver.join();
176+
}
177+
147178
#endif /* WIN32 */
148179

149180
BOOST_AUTO_TEST_SUITE_END()

src/util/sock.cpp

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -175,7 +175,8 @@ void Sock::SendComplete(const std::string& data,
175175

176176
std::string Sock::RecvUntilTerminator(uint8_t terminator,
177177
std::chrono::milliseconds timeout,
178-
CThreadInterrupt& interrupt) const
178+
CThreadInterrupt& interrupt,
179+
size_t max_data) const
179180
{
180181
const auto deadline = GetTime<std::chrono::milliseconds>() + timeout;
181182
std::string data;
@@ -190,9 +191,14 @@ std::string Sock::RecvUntilTerminator(uint8_t terminator,
190191
// at a time is about 50 times slower.
191192

192193
for (;;) {
194+
if (data.size() >= max_data) {
195+
throw std::runtime_error(
196+
strprintf("Received too many bytes without a terminator (%u)", data.size()));
197+
}
198+
193199
char buf[512];
194200

195-
const ssize_t peek_ret{Recv(buf, sizeof(buf), MSG_PEEK)};
201+
const ssize_t peek_ret{Recv(buf, std::min(sizeof(buf), max_data - data.size()), MSG_PEEK)};
196202

197203
switch (peek_ret) {
198204
case -1: {

src/util/sock.h

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -135,13 +135,16 @@ class Sock
135135
* @param[in] terminator Character up to which to read from the socket.
136136
* @param[in] timeout Timeout for the entire operation.
137137
* @param[in] interrupt If this is signaled then the operation is canceled.
138+
* @param[in] max_data The maximum amount of data (in bytes) to receive. If this many bytes
139+
* are received and there is still no terminator, then this method will throw an exception.
138140
* @return The data that has been read, without the terminating character.
139141
* @throws std::runtime_error if the operation cannot be completed. In this case some bytes may
140142
* have been consumed from the socket.
141143
*/
142144
virtual std::string RecvUntilTerminator(uint8_t terminator,
143145
std::chrono::milliseconds timeout,
144-
CThreadInterrupt& interrupt) const;
146+
CThreadInterrupt& interrupt,
147+
size_t max_data) const;
145148

146149
/**
147150
* Check if still connected.

0 commit comments

Comments
 (0)