Skip to content

Commit 37e7110

Browse files
committed
touch up atomic read limiter
1 parent 83c76be commit 37e7110

File tree

5 files changed

+66
-38
lines changed

5 files changed

+66
-38
lines changed

capnp-futures/src/serialize.rs

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -95,10 +95,12 @@ async fn read_segment_table<R>(mut reader: R,
9595
// Don't accept a message which the receiver couldn't possibly traverse without hitting the
9696
// traversal limit. Without this check, a malicious client could transmit a very large segment
9797
// size to make the receiver allocate excessive space and possibly crash.
98-
if segment_lengths_builder.total_words() as u64 > options.traversal_limit_in_words {
99-
return Err(Error::failed(
100-
format!("Message has {} words, which is too large. To increase the limit on the \
101-
receiving end, see capnp::message::ReaderOptions.", segment_lengths_builder.total_words())))
98+
if let Some(traversal_limit_in_words) = options.traversal_limit_in_words {
99+
if segment_lengths_builder.total_words() > traversal_limit_in_words {
100+
return Err(Error::failed(
101+
format!("Message has {} words, which is too large. To increase the limit on the \
102+
receiving end, see capnp::message::ReaderOptions.", segment_lengths_builder.total_words())))
103+
}
102104
}
103105

104106
Ok(Some(segment_lengths_builder))

capnp/src/message.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ pub struct ReaderOptions {
5050
/// Together with sensible coding practices (e.g. trying to avoid calling sub-object getters
5151
/// multiple times, which is expensive anyway), this should provide adequate protection without
5252
/// inconvenience.
53-
pub traversal_limit_in_words: u64,
53+
pub traversal_limit_in_words: Option<usize>,
5454

5555
/// Limits how deeply nested a message structure can be, e.g. structs containing other structs or
5656
/// lists of structs.
@@ -64,7 +64,7 @@ pub struct ReaderOptions {
6464
}
6565

6666
pub const DEFAULT_READER_OPTIONS: ReaderOptions =
67-
ReaderOptions { traversal_limit_in_words: 8 * 1024 * 1024, nesting_limit: 64 };
67+
ReaderOptions { traversal_limit_in_words: Some(8 * 1024 * 1024), nesting_limit: 64 };
6868

6969

7070
impl Default for ReaderOptions {
@@ -81,7 +81,7 @@ impl ReaderOptions {
8181
self
8282
}
8383

84-
pub fn traversal_limit_in_words<'a>(&'a mut self, value: u64) -> &'a mut ReaderOptions {
84+
pub fn traversal_limit_in_words<'a>(&'a mut self, value: Option<usize>) -> &'a mut ReaderOptions {
8585
self.traversal_limit_in_words = value;
8686
self
8787
}
@@ -382,7 +382,7 @@ impl <A> Builder<A> where A: Allocator {
382382

383383
pub fn into_reader(self) -> Reader<Builder<A>> {
384384
Reader::new(self, ReaderOptions {
385-
traversal_limit_in_words: u64::max_value(),
385+
traversal_limit_in_words: None,
386386
nesting_limit: i32::max_value()
387387
})
388388
}

capnp/src/private/read_limiter.rs

Lines changed: 49 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -28,35 +28,45 @@ mod sync {
2828
use core::sync::atomic::{AtomicUsize, Ordering};
2929

3030
pub struct ReadLimiter {
31-
pub limit: AtomicUsize,
31+
limit: AtomicUsize,
32+
error_on_limit_exceeded: bool,
3233
}
3334

3435
impl ReadLimiter {
35-
pub fn new(limit: u64) -> ReadLimiter {
36-
if limit > core::usize::MAX as u64 {
37-
panic!("traversal_limit_in_words cannot be bigger than core::usize::MAX")
38-
}
39-
40-
ReadLimiter {
41-
limit: AtomicUsize::new(limit as usize),
36+
pub fn new(limit: Option<usize>) -> ReadLimiter {
37+
match limit {
38+
Some(value) => {
39+
ReadLimiter {
40+
limit: AtomicUsize::new(value),
41+
error_on_limit_exceeded: true,
42+
}
43+
}
44+
None => {
45+
ReadLimiter {
46+
limit: AtomicUsize::new(usize::MAX),
47+
error_on_limit_exceeded: false,
48+
}
49+
}
4250
}
4351
}
4452

4553
#[inline]
4654
pub fn can_read(&self, amount: usize) -> Result<()> {
47-
let cur_limit = self.limit.load(Ordering::Relaxed);
48-
if cur_limit < amount {
49-
return Err(Error::failed(format!("read limit exceeded")));
50-
}
55+
// We use separate AtomicUsize::load() and AtomicUsize::store() steps, which may
56+
// result in undercounting reads if multiple threads are reading at the same.
57+
// That's okay -- a denial of service attack will eventually hit the limit anyway.
58+
//
59+
// We could instead do a single fetch_sub() step, but that seems to be slower.
5160

52-
let prev_limit = self.limit.fetch_sub(amount, Ordering::Relaxed);
53-
if prev_limit < amount {
54-
// if the previous limit was lower than the amount we read, the limit has underflowed
55-
// and wrapped around so we need to reset it to 0 for next reader to fail
56-
self.limit.store(0, Ordering::Relaxed);
61+
let current = self.limit.load(Ordering::Relaxed);
62+
if amount > current && self.error_on_limit_exceeded {
5763
return Err(Error::failed(format!("read limit exceeded")));
64+
} else {
65+
// The common case is current >= amount. Note that we only branch once in that case.
66+
// If we combined the fields into an Option<AtomicUsize>, we would
67+
// need to branch twice in the common case.
68+
self.limit.store(current.wrapping_sub(amount), Ordering::Relaxed);
5869
}
59-
6070
Ok(())
6171
}
6272
}
@@ -71,24 +81,38 @@ mod unsync {
7181
use core::cell::Cell;
7282

7383
pub struct ReadLimiter {
74-
pub limit: Cell<u64>,
84+
limit: Cell<usize>,
85+
error_on_limit_exceeded: bool,
7586
}
7687

7788
impl ReadLimiter {
78-
pub fn new(limit: u64) -> ReadLimiter {
79-
ReadLimiter {
80-
limit: Cell::new(limit),
89+
pub fn new(limit: Option<usize>) -> ReadLimiter {
90+
match limit {
91+
Some(value) => {
92+
ReadLimiter {
93+
limit: Cell::new(value),
94+
error_on_limit_exceeded: true,
95+
}
96+
}
97+
None => {
98+
ReadLimiter {
99+
limit: Cell::new(usize::MAX),
100+
error_on_limit_exceeded: false,
101+
}
102+
}
81103
}
82104
}
83105

84106
#[inline]
85107
pub fn can_read(&self, amount: usize) -> Result<()> {
86-
let amount = amount as u64;
87108
let current = self.limit.get();
88-
if amount > current {
109+
if amount > current && self.error_on_limit_exceeded {
89110
Err(Error::failed(format!("read limit exceeded")))
90111
} else {
91-
self.limit.set(current - amount);
112+
// The common case is current >= amount. Note that we only branch once in that case.
113+
// If we combined the fields into an Option<Cell<usize>>, we would
114+
// need to branch twice in the common case.
115+
self.limit.set(current.wrapping_sub(amount));
92116
Ok(())
93117
}
94118
}

capnp/src/serialize.rs

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -257,10 +257,12 @@ fn read_segment_table<R>(read: &mut R,
257257
// Don't accept a message which the receiver couldn't possibly traverse without hitting the
258258
// traversal limit. Without this check, a malicious client could transmit a very large segment
259259
// size to make the receiver allocate excessive space and possibly crash.
260-
if segment_lengths_builder.total_words() as u64 > options.traversal_limit_in_words {
261-
return Err(Error::failed(
262-
format!("Message has {} words, which is too large. To increase the limit on the \
263-
receiving end, see capnp::message::ReaderOptions.", segment_lengths_builder.total_words())))
260+
if let Some(limit) = options.traversal_limit_in_words {
261+
if segment_lengths_builder.total_words() > limit {
262+
return Err(Error::failed(
263+
format!("Message has {} words, which is too large. To increase the limit on the \
264+
receiving end, see capnp::message::ReaderOptions.", segment_lengths_builder.total_words())))
265+
}
264266
}
265267

266268
Ok(Some(segment_lengths_builder))

capnpc/test/test.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1250,7 +1250,7 @@ mod tests {
12501250

12511251
let segments = message.get_segments_for_output();
12521252
let reader = message::Reader::new(message::SegmentArray::new(&segments),
1253-
*ReaderOptions::new().traversal_limit_in_words(2));
1253+
*ReaderOptions::new().traversal_limit_in_words(Some(2)));
12541254
match reader.get_root::<test_all_types::Reader>() {
12551255
Err(e) => assert_eq!(e.description, "read limit exceeded"),
12561256
Ok(_) => panic!("expected error"),

0 commit comments

Comments
 (0)