Skip to content

Commit 82ef302

Browse files
committed
limit electrum rpc request line length
- limit electrum rpc requests to 1 MiB default - provide option to increase limit
1 parent 4615126 commit 82ef302

3 files changed

Lines changed: 199 additions & 8 deletions

File tree

‎src/config.rs‎

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ pub struct Config {
6060
pub cookie: Option<SensitiveAuth>,
6161
pub electrum_rpc_addr: SocketAddr,
6262
pub electrum_rpc_conn_max_age: Option<Duration>,
63+
pub electrum_rpc_max_request_num_bytes: usize,
6364
pub http_addr: SocketAddr,
6465
pub http_socket_file: Option<PathBuf>,
6566
pub monitoring_addr: SocketAddr,
@@ -193,6 +194,13 @@ impl Config {
193194
.default_value("0")
194195
.takes_value(true),
195196
)
197+
.arg(
198+
Arg::with_name("electrum_rpc_max_request_num_bytes")
199+
.long("electrum-rpc-max-request-num-bytes")
200+
.help("Maximum size (in bytes) of a single Electrum RPC request line. A client streaming bytes without a newline is disconnected once its in-flight line exceeds this size, bounding per-connection memory. 0 = unlimited (default: 1048576, i.e. 1 MiB)")
201+
.default_value("1048576")
202+
.takes_value(true),
203+
)
196204
.arg(
197205
Arg::with_name("http_addr")
198206
.long("http-addr")
@@ -499,6 +507,14 @@ impl Config {
499507
0 => None, // 0 = unlimited / never disconnect
500508
secs => Some(Duration::from_secs(secs)),
501509
};
510+
let electrum_rpc_max_request_num_bytes: usize = match value_t_or_exit!(
511+
m,
512+
"electrum_rpc_max_request_num_bytes",
513+
usize
514+
) {
515+
0 => usize::MAX, // 0 = unlimited
516+
bytes => bytes,
517+
};
502518
let http_addr: SocketAddr = str_to_socketaddr(
503519
m.value_of("http_addr")
504520
.unwrap_or(&format!("127.0.0.1:{}", default_http_port)),
@@ -570,6 +586,7 @@ impl Config {
570586
utxos_limit: value_t_or_exit!(m, "utxos_limit", usize),
571587
electrum_rpc_addr,
572588
electrum_rpc_conn_max_age,
589+
electrum_rpc_max_request_num_bytes,
573590
electrum_txs_limit: value_t_or_exit!(m, "electrum_txs_limit", usize),
574591
electrum_subscription_limit: value_t_or_exit!(m, "electrum_subscription_limit", usize),
575592
electrum_banner,

‎src/electrum/server.rs‎

Lines changed: 181 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,7 @@ struct Connection {
182182
stats: Arc<Stats>,
183183
txs_limit: usize,
184184
subscription_limit: usize,
185+
max_request_bytes: usize,
185186
#[cfg(feature = "electrum-discovery")]
186187
discovery: Option<Arc<DiscoveryManager>>,
187188
rpc_logging: RpcLogging,
@@ -204,6 +205,7 @@ impl Connection {
204205
stats: Arc<Stats>,
205206
txs_limit: usize,
206207
subscription_limit: usize,
208+
max_request_bytes: usize,
207209
#[cfg(feature = "electrum-discovery")] discovery: Option<Arc<DiscoveryManager>>,
208210
rpc_logging: RpcLogging,
209211
salt: String,
@@ -218,6 +220,7 @@ impl Connection {
218220
stats,
219221
txs_limit,
220222
subscription_limit,
223+
max_request_bytes,
221224
#[cfg(feature = "electrum-discovery")]
222225
discovery,
223226
rpc_logging,
@@ -784,13 +787,51 @@ impl Connection {
784787
}
785788
}
786789

790+
fn read_bounded_line(reader: &mut BufReader<TcpStream>, max_len: usize) -> Result<Vec<u8>> {
791+
let mut line = Vec::<u8>::new();
792+
loop {
793+
let (done, consumed) = {
794+
let available = reader.fill_buf().chain_err(|| "failed to read a request")?;
795+
if available.is_empty() {
796+
(true, 0) // EOF
797+
} else if let Some(pos) = available.iter().position(|&b| b == b'\n') {
798+
let new_len = line
799+
.len()
800+
.checked_add(pos + 1)
801+
.ok_or_else(|| "request line length overflow")?;
802+
if new_len > max_len {
803+
bail!("request line exceeds maximum size of {} bytes", max_len);
804+
}
805+
line.extend_from_slice(&available[..=pos]);
806+
(true, pos + 1)
807+
} else {
808+
let new_len = line
809+
.len()
810+
.checked_add(available.len())
811+
.ok_or_else(|| "request line length overflow")?;
812+
if new_len > max_len {
813+
bail!("request line exceeds maximum size of {} bytes", max_len);
814+
}
815+
let take = available.len();
816+
line.extend_from_slice(&available[..take]);
817+
(false, take)
818+
}
819+
};
820+
reader.consume(consumed);
821+
if done {
822+
return Ok(line);
823+
}
824+
}
825+
}
826+
787827
#[trace]
788-
fn parse_requests(mut reader: BufReader<TcpStream>, tx: &SyncSender<Message>) -> Result<()> {
828+
fn parse_requests(
829+
mut reader: BufReader<TcpStream>,
830+
tx: &SyncSender<Message>,
831+
max_request_bytes: usize,
832+
) -> Result<()> {
789833
loop {
790-
let mut line = Vec::<u8>::new();
791-
reader
792-
.read_until(b'\n', &mut line)
793-
.chain_err(|| "failed to read a request")?;
834+
let line = Connection::read_bounded_line(&mut reader, max_request_bytes)?;
794835
if line.is_empty() {
795836
return Ok(());
796837
} else {
@@ -810,8 +851,12 @@ impl Connection {
810851
}
811852
}
812853

813-
fn reader_thread(reader: BufReader<TcpStream>, tx: SyncSender<Message>) -> Result<()> {
814-
let result = Connection::parse_requests(reader, &tx);
854+
fn reader_thread(
855+
reader: BufReader<TcpStream>,
856+
tx: SyncSender<Message>,
857+
max_request_bytes: usize,
858+
) -> Result<()> {
859+
let result = Connection::parse_requests(reader, &tx, max_request_bytes);
815860
if let Err(e) = tx.send(Message::Done) {
816861
// The writer already tore the channel down (e.g. after a write
817862
// error or connection expiry) — expected during teardown races.
@@ -826,7 +871,10 @@ impl Connection {
826871

827872
let reader = BufReader::new(self.stream.try_clone().expect("failed to clone TcpStream"));
828873
let sender = self.sender.clone();
829-
let child = spawn_thread("reader", || Connection::reader_thread(reader, sender));
874+
let max_request_bytes = self.max_request_bytes;
875+
let child = spawn_thread("reader", move || {
876+
Connection::reader_thread(reader, sender, max_request_bytes)
877+
});
830878
if let Err(e) = self.handle_replies(receiver) {
831879
if is_disconnect(&e) {
832880
// client went away mid-exchange (broken pipe / reset) — not actionable
@@ -1189,6 +1237,7 @@ impl RPC {
11891237
let rpc_addr = config.electrum_rpc_addr;
11901238
let txs_limit = config.electrum_txs_limit;
11911239
let subscription_limit = config.electrum_subscription_limit;
1240+
let max_request_bytes = config.electrum_rpc_max_request_num_bytes;
11921241
let conn_max_age = config.electrum_rpc_conn_max_age;
11931242

11941243
RPC {
@@ -1240,6 +1289,7 @@ impl RPC {
12401289
stats,
12411290
txs_limit,
12421291
subscription_limit,
1292+
max_request_bytes,
12431293
#[cfg(feature = "electrum-discovery")]
12441294
discovery,
12451295
rpc_logging,
@@ -1512,4 +1562,127 @@ mod tests {
15121562
drop(stream);
15131563
assert!(weak.upgrade().is_none());
15141564
}
1565+
1566+
#[test]
1567+
fn read_bounded_line_reads_a_normal_line() {
1568+
let listener = TcpListener::bind("127.0.0.1:0").unwrap();
1569+
let mut client = TcpStream::connect(listener.local_addr().unwrap()).unwrap();
1570+
let mut reader = BufReader::new(listener.accept().unwrap().0);
1571+
1572+
client.write_all(b"hello world\n").unwrap();
1573+
let line = Connection::read_bounded_line(&mut reader, 1024).unwrap();
1574+
assert_eq!(line, b"hello world\n");
1575+
}
1576+
1577+
#[test]
1578+
fn read_bounded_line_rejects_a_line_without_a_newline_past_the_limit() {
1579+
let listener = TcpListener::bind("127.0.0.1:0").unwrap();
1580+
let mut client = TcpStream::connect(listener.local_addr().unwrap()).unwrap();
1581+
let mut reader = BufReader::new(listener.accept().unwrap().0);
1582+
1583+
// Stream well past the limit with no '\n': the never-terminated-line
1584+
// OOM this guards against.
1585+
let chunk = [b'A'; 4096];
1586+
let writer = thread::spawn(move || {
1587+
for _ in 0..64 {
1588+
if client.write_all(&chunk).is_err() {
1589+
break;
1590+
}
1591+
}
1592+
});
1593+
1594+
let err = Connection::read_bounded_line(&mut reader, 1024).unwrap_err();
1595+
assert!(err.to_string().contains("exceeds maximum size"));
1596+
let _ = writer.join();
1597+
}
1598+
1599+
#[test]
1600+
fn read_bounded_line_rejects_a_line_whose_terminating_newline_arrives_over_the_limit() {
1601+
let listener = TcpListener::bind("127.0.0.1:0").unwrap();
1602+
let mut client = TcpStream::connect(listener.local_addr().unwrap()).unwrap();
1603+
let mut reader = BufReader::new(listener.accept().unwrap().0);
1604+
1605+
// The '\n' lands in the same fill_buf() chunk that pushes the
1606+
// accumulated line past the limit, so `done` is true on the very
1607+
// iteration where the size check must fire.
1608+
let mut payload = vec![b'A'; 2048];
1609+
payload.push(b'\n');
1610+
client.write_all(&payload).unwrap();
1611+
1612+
let err = Connection::read_bounded_line(&mut reader, 1024).unwrap_err();
1613+
assert!(err.to_string().contains("exceeds maximum size"));
1614+
}
1615+
1616+
#[test]
1617+
fn read_bounded_line_returns_empty_on_immediate_eof() {
1618+
let listener = TcpListener::bind("127.0.0.1:0").unwrap();
1619+
let client = TcpStream::connect(listener.local_addr().unwrap()).unwrap();
1620+
let mut reader = BufReader::new(listener.accept().unwrap().0);
1621+
1622+
drop(client);
1623+
let line = Connection::read_bounded_line(&mut reader, 1024).unwrap();
1624+
assert!(line.is_empty());
1625+
}
1626+
1627+
#[test]
1628+
fn read_bounded_line_returns_partial_line_on_eof_without_newline() {
1629+
let listener = TcpListener::bind("127.0.0.1:0").unwrap();
1630+
let mut client = TcpStream::connect(listener.local_addr().unwrap()).unwrap();
1631+
let mut reader = BufReader::new(listener.accept().unwrap().0);
1632+
1633+
client.write_all(b"no newline here").unwrap();
1634+
drop(client);
1635+
1636+
let line = Connection::read_bounded_line(&mut reader, 1024).unwrap();
1637+
assert_eq!(line, b"no newline here");
1638+
}
1639+
1640+
#[test]
1641+
fn read_bounded_line_accepts_a_line_exactly_at_the_limit() {
1642+
let listener = TcpListener::bind("127.0.0.1:0").unwrap();
1643+
let mut client = TcpStream::connect(listener.local_addr().unwrap()).unwrap();
1644+
let mut reader = BufReader::new(listener.accept().unwrap().0);
1645+
1646+
let payload = vec![b'A'; 1024];
1647+
client.write_all(&payload).unwrap();
1648+
drop(client);
1649+
1650+
let line = Connection::read_bounded_line(&mut reader, 1024).unwrap();
1651+
assert_eq!(line, payload);
1652+
}
1653+
1654+
#[test]
1655+
fn read_bounded_line_rejects_a_single_chunk_exceeding_a_small_limit() {
1656+
let listener = TcpListener::bind("127.0.0.1:0").unwrap();
1657+
let mut client = TcpStream::connect(listener.local_addr().unwrap()).unwrap();
1658+
let mut reader = BufReader::new(listener.accept().unwrap().0);
1659+
1660+
// A single write, well within the reader's default 8KiB buffer, that
1661+
// still lands entirely in one fill_buf() call and exceeds a limit
1662+
// much smaller than that buffer.
1663+
let payload = vec![b'A'; 500];
1664+
client.write_all(&payload).unwrap();
1665+
1666+
let err = Connection::read_bounded_line(&mut reader, 100).unwrap_err();
1667+
assert!(err.to_string().contains("exceeds maximum size"));
1668+
}
1669+
1670+
#[test]
1671+
fn read_bounded_line_handles_unlimited_max_len_split_across_writes() {
1672+
// max_len == usize::MAX is what a configured value of 0 ("unlimited")
1673+
// maps to. Splitting the request and its terminating newline across
1674+
// separate writes forces at least one fill_buf() call to return a
1675+
// newline-less chunk, which used to compute `max_len + 1` and
1676+
// overflow.
1677+
let listener = TcpListener::bind("127.0.0.1:0").unwrap();
1678+
let mut client = TcpStream::connect(listener.local_addr().unwrap()).unwrap();
1679+
let mut reader = BufReader::new(listener.accept().unwrap().0);
1680+
1681+
client.write_all(b"server.ping").unwrap();
1682+
thread::sleep(Duration::from_millis(50));
1683+
client.write_all(b"\n").unwrap();
1684+
1685+
let line = Connection::read_bounded_line(&mut reader, usize::MAX).unwrap();
1686+
assert_eq!(line, b"server.ping\n");
1687+
}
15151688
}

‎tests/common.rs‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@ impl TestRunner {
101101
cookie: None,
102102
electrum_rpc_addr: rand_available_addr(),
103103
electrum_rpc_conn_max_age: None,
104+
electrum_rpc_max_request_num_bytes: 1_048_576,
104105
http_addr: rand_available_addr(),
105106
http_socket_file: None, // XXX test with socket file or tcp?
106107
monitoring_addr: rand_available_addr(),

0 commit comments

Comments
 (0)