Implement proposal 318: Limit protovers to 0..63

In brief: we go through a lot of gymnastics to handle huge protover
numbers, but after years of development we're not even close to 10
for any of our current versions.  We also have a convenient
workaround available in case we ever run out of protocols: if (for
example) we someday need Link=64, we can just add Link2=0 or
something.

This patch is a minimal patch to change tor's behavior; it doesn't
take advantage of the new restrictions.

Implements #40133 and proposal 318.
This commit is contained in:
Nick Mathewson 2020-10-14 11:28:37 -04:00
parent 741edf1b45
commit dd63b97288
7 changed files with 66 additions and 126 deletions

5
changes/ticket40133 Normal file
View File

@ -0,0 +1,5 @@
o Minor features (protocol simplification):
- Tor no longer allows subprotocol versions larger than 63. Previously
versions up to UINT32_MAX were allowed, which significantly complicated
our code.
Implements proposal 318; closes ticket 40133.

View File

@ -113,13 +113,13 @@ proto_entry_free_(proto_entry_t *entry)
} }
/** The largest possible protocol version. */ /** The largest possible protocol version. */
#define MAX_PROTOCOL_VERSION (UINT32_MAX-1) #define MAX_PROTOCOL_VERSION (63)
/** /**
* Given a string <b>s</b> and optional end-of-string pointer * Given a string <b>s</b> and optional end-of-string pointer
* <b>end_of_range</b>, parse the protocol range and store it in * <b>end_of_range</b>, parse the protocol range and store it in
* <b>low_out</b> and <b>high_out</b>. A protocol range has the format U, or * <b>low_out</b> and <b>high_out</b>. A protocol range has the format U, or
* U-U, where U is an unsigned 32-bit integer. * U-U, where U is an unsigned integer between 0 and 63 inclusive.
*/ */
static int static int
parse_version_range(const char *s, const char *end_of_range, parse_version_range(const char *s, const char *end_of_range,

View File

@ -36,7 +36,7 @@ impl Display for ProtoverError {
ProtoverError::Unparseable => write!(f, "The protover string was unparseable."), ProtoverError::Unparseable => write!(f, "The protover string was unparseable."),
ProtoverError::ExceedsMax => write!( ProtoverError::ExceedsMax => write!(
f, f,
"The high in a (low, high) protover range exceeds u32::MAX." "The high in a (low, high) protover range exceeds 63."
), ),
ProtoverError::ExceedsExpansionLimit => write!( ProtoverError::ExceedsExpansionLimit => write!(
f, f,

View File

@ -294,6 +294,10 @@ impl ProtoSet {
} }
} }
/// Largest allowed protocol version.
/// C_RUST_COUPLED: protover.c `MAX_PROTOCOL_VERSION`
const MAX_PROTOCOL_VERSION: Version = 63;
impl FromStr for ProtoSet { impl FromStr for ProtoSet {
type Err = ProtoverError; type Err = ProtoverError;
@ -370,7 +374,7 @@ impl FromStr for ProtoSet {
let pieces: ::std::str::Split<char> = version_string.split(','); let pieces: ::std::str::Split<char> = version_string.split(',');
for p in pieces { for p in pieces {
if p.contains('-') { let (lo,hi) = if p.contains('-') {
let mut pair = p.splitn(2, '-'); let mut pair = p.splitn(2, '-');
let low = pair.next().ok_or(ProtoverError::Unparseable)?; let low = pair.next().ok_or(ProtoverError::Unparseable)?;
@ -379,12 +383,17 @@ impl FromStr for ProtoSet {
let lo: Version = low.parse().or(Err(ProtoverError::Unparseable))?; let lo: Version = low.parse().or(Err(ProtoverError::Unparseable))?;
let hi: Version = high.parse().or(Err(ProtoverError::Unparseable))?; let hi: Version = high.parse().or(Err(ProtoverError::Unparseable))?;
pairs.push((lo, hi)); (lo,hi)
} else { } else {
let v: u32 = p.parse().or(Err(ProtoverError::Unparseable))?; let v: u32 = p.parse().or(Err(ProtoverError::Unparseable))?;
pairs.push((v, v)); (v, v)
};
if lo > MAX_PROTOCOL_VERSION || hi > MAX_PROTOCOL_VERSION {
return Err(ProtoverError::ExceedsMax);
} }
pairs.push((lo, hi));
} }
ProtoSet::from_slice(&pairs[..]) ProtoSet::from_slice(&pairs[..])
@ -674,12 +683,11 @@ mod test {
#[test] #[test]
fn test_protoset_into_vec() { fn test_protoset_into_vec() {
let ps: ProtoSet = "1-13,42,9001,4294967294".parse().unwrap(); let ps: ProtoSet = "1-13,42".parse().unwrap();
let v: Vec<Version> = ps.into(); let v: Vec<Version> = ps.into();
assert!(v.contains(&7)); assert!(v.contains(&7));
assert!(v.contains(&9001)); assert!(v.contains(&42));
assert!(v.contains(&4294967294));
} }
} }

View File

@ -866,12 +866,12 @@ mod test {
#[test] #[test]
fn test_protoentry_from_str_allowed_number_of_versions() { fn test_protoentry_from_str_allowed_number_of_versions() {
assert_protoentry_is_parseable!("Desc=1-4294967294"); assert_protoentry_is_parseable!("Desc=1-63");
} }
#[test] #[test]
fn test_protoentry_from_str_too_many_versions() { fn test_protoentry_from_str_too_many_versions() {
assert_protoentry_is_unparseable!("Desc=1-4294967295"); assert_protoentry_is_unparseable!("Desc=1-64");
} }
#[test] #[test]
@ -910,10 +910,10 @@ mod test {
#[test] #[test]
fn test_protoentry_all_supported_unsupported_high_version() { fn test_protoentry_all_supported_unsupported_high_version() {
let protocols: UnvalidatedProtoEntry = "HSDir=12-100".parse().unwrap(); let protocols: UnvalidatedProtoEntry = "HSDir=12-60".parse().unwrap();
let unsupported: Option<UnvalidatedProtoEntry> = protocols.all_supported(); let unsupported: Option<UnvalidatedProtoEntry> = protocols.all_supported();
assert_eq!(true, unsupported.is_some()); assert_eq!(true, unsupported.is_some());
assert_eq!("HSDir=12-100", &unsupported.unwrap().to_string()); assert_eq!("HSDir=12-60", &unsupported.unwrap().to_string());
} }
#[test] #[test]
@ -962,7 +962,7 @@ mod test {
ProtoSet::from_str(&versions).unwrap().to_string() ProtoSet::from_str(&versions).unwrap().to_string()
); );
versions = "1-3,500"; versions = "1-3,50";
assert_eq!( assert_eq!(
String::from(versions), String::from(versions),
ProtoSet::from_str(&versions).unwrap().to_string() ProtoSet::from_str(&versions).unwrap().to_string()

View File

@ -98,10 +98,10 @@ fn protocol_all_supported_with_unsupported_protocol() {
#[test] #[test]
fn protocol_all_supported_with_unsupported_versions() { fn protocol_all_supported_with_unsupported_versions() {
let protocols: UnvalidatedProtoEntry = "Link=3-999".parse().unwrap(); let protocols: UnvalidatedProtoEntry = "Link=3-63".parse().unwrap();
let unsupported: Option<UnvalidatedProtoEntry> = protocols.all_supported(); let unsupported: Option<UnvalidatedProtoEntry> = protocols.all_supported();
assert_eq!(true, unsupported.is_some()); assert_eq!(true, unsupported.is_some());
assert_eq!("Link=6-999", &unsupported.unwrap().to_string()); assert_eq!("Link=6-63", &unsupported.unwrap().to_string());
} }
#[test] #[test]
@ -114,10 +114,10 @@ fn protocol_all_supported_with_unsupported_low_version() {
#[test] #[test]
fn protocol_all_supported_with_unsupported_high_version() { fn protocol_all_supported_with_unsupported_high_version() {
let protocols: UnvalidatedProtoEntry = "Cons=1-2,999".parse().unwrap(); let protocols: UnvalidatedProtoEntry = "Cons=1-2,60".parse().unwrap();
let unsupported: Option<UnvalidatedProtoEntry> = protocols.all_supported(); let unsupported: Option<UnvalidatedProtoEntry> = protocols.all_supported();
assert_eq!(true, unsupported.is_some()); assert_eq!(true, unsupported.is_some());
assert_eq!("Cons=999", &unsupported.unwrap().to_string()); assert_eq!("Cons=60", &unsupported.unwrap().to_string());
} }
#[test] #[test]
@ -195,27 +195,27 @@ fn protover_compute_vote_returns_protocols_that_it_doesnt_currently_support() {
#[test] #[test]
fn protover_compute_vote_returns_matching_for_mix() { fn protover_compute_vote_returns_matching_for_mix() {
let protocols: &[UnvalidatedProtoEntry] = &["Link=1-10,500 Cons=1,3-7,8".parse().unwrap()]; let protocols: &[UnvalidatedProtoEntry] = &["Link=1-10,50 Cons=1,3-7,8".parse().unwrap()];
let listed = ProtoverVote::compute(protocols, &1); let listed = ProtoverVote::compute(protocols, &1);
assert_eq!("Cons=1,3-8 Link=1-10,500", listed.to_string()); assert_eq!("Cons=1,3-8 Link=1-10,50", listed.to_string());
} }
#[test] #[test]
fn protover_compute_vote_returns_matching_for_longer_mix() { fn protover_compute_vote_returns_matching_for_longer_mix() {
let protocols: &[UnvalidatedProtoEntry] = &[ let protocols: &[UnvalidatedProtoEntry] = &[
"Desc=1-10,500 Cons=1,3-7,8".parse().unwrap(), "Desc=1-10,50 Cons=1,3-7,8".parse().unwrap(),
"Link=123-456,78 Cons=2-6,8 Desc=9".parse().unwrap(), "Link=12-45,8 Cons=2-6,8 Desc=9".parse().unwrap(),
]; ];
let listed = ProtoverVote::compute(protocols, &1); let listed = ProtoverVote::compute(protocols, &1);
assert_eq!("Cons=1-8 Desc=1-10,500 Link=78,123-456", listed.to_string()); assert_eq!("Cons=1-8 Desc=1-10,50 Link=8,12-45", listed.to_string());
} }
#[test] #[test]
fn protover_compute_vote_returns_matching_for_longer_mix_with_threshold_two() { fn protover_compute_vote_returns_matching_for_longer_mix_with_threshold_two() {
let protocols: &[UnvalidatedProtoEntry] = &[ let protocols: &[UnvalidatedProtoEntry] = &[
"Desc=1-10,500 Cons=1,3-7,8".parse().unwrap(), "Desc=1-10,50 Cons=1,3-7,8".parse().unwrap(),
"Link=123-456,78 Cons=2-6,8 Desc=9".parse().unwrap(), "Link=8,12-45 Cons=2-6,8 Desc=9".parse().unwrap(),
]; ];
let listed = ProtoverVote::compute(protocols, &2); let listed = ProtoverVote::compute(protocols, &2);
@ -320,30 +320,20 @@ fn protocol_all_supported_with_single_protocol_and_protocol_range() {
assert_eq!(true, unsupported.is_none()); assert_eq!(true, unsupported.is_none());
} }
// By allowing us to add to votes, the C implementation allows us to
// exceed the limit.
#[test]
fn protover_compute_vote_may_exceed_limit() {
let proto1: UnvalidatedProtoEntry = "Sleen=1-65535".parse().unwrap();
let proto2: UnvalidatedProtoEntry = "Sleen=100000".parse().unwrap();
let _result: UnvalidatedProtoEntry = ProtoverVote::compute(&[proto1, proto2], &1);
}
#[test] #[test]
fn protover_all_supported_should_exclude_versions_we_actually_do_support() { fn protover_all_supported_should_exclude_versions_we_actually_do_support() {
let proto: UnvalidatedProtoEntry = "Link=3-999".parse().unwrap(); let proto: UnvalidatedProtoEntry = "Link=3-63".parse().unwrap();
let result: String = proto.all_supported().unwrap().to_string(); let result: String = proto.all_supported().unwrap().to_string();
assert_eq!(result, "Link=6-999".to_string()); assert_eq!(result, "Link=6-63".to_string());
} }
#[test] #[test]
fn protover_all_supported_should_exclude_versions_we_actually_do_support_complex1() { fn protover_all_supported_should_exclude_versions_we_actually_do_support_complex1() {
let proto: UnvalidatedProtoEntry = "Link=1-3,345-666".parse().unwrap(); let proto: UnvalidatedProtoEntry = "Link=1-3,30-63".parse().unwrap();
let result: String = proto.all_supported().unwrap().to_string(); let result: String = proto.all_supported().unwrap().to_string();
assert_eq!(result, "Link=345-666".to_string()); assert_eq!(result, "Link=30-63".to_string());
} }
#[test] #[test]
@ -356,26 +346,10 @@ fn protover_all_supported_should_exclude_versions_we_actually_do_support_complex
#[test] #[test]
fn protover_all_supported_should_exclude_some_versions_and_entire_protocols() { fn protover_all_supported_should_exclude_some_versions_and_entire_protocols() {
let proto: UnvalidatedProtoEntry = "Link=1-3,5-12 Quokka=9000-9001".parse().unwrap(); let proto: UnvalidatedProtoEntry = "Link=1-3,5-12 Quokka=50-51".parse().unwrap();
let result: String = proto.all_supported().unwrap().to_string(); let result: String = proto.all_supported().unwrap().to_string();
assert_eq!(result, "Link=6-12 Quokka=9000-9001".to_string()); assert_eq!(result, "Link=6-12 Quokka=50-51".to_string());
}
#[test]
fn protover_all_supported_should_not_dos_anyones_computer() {
let proto: UnvalidatedProtoEntry = "Link=1-2147483648".parse().unwrap();
let result: String = proto.all_supported().unwrap().to_string();
assert_eq!(result, "Link=6-2147483648".to_string());
}
#[test]
fn protover_all_supported_should_not_dos_anyones_computer_max_versions() {
let proto: UnvalidatedProtoEntry = "Link=1-4294967294".parse().unwrap();
let result: String = proto.all_supported().unwrap().to_string();
assert_eq!(result, "Link=6-4294967294".to_string());
} }
#[test] #[test]

View File

@ -25,7 +25,7 @@ test_protover_parse(void *arg)
#else #else
char *re_encoded = NULL; char *re_encoded = NULL;
const char *orig = "Foo=1,3 Bar=3 Baz= Quux=9-12,14,15-16,900"; const char *orig = "Foo=1,3 Bar=3 Baz= Quux=9-12,14,15-16";
smartlist_t *elts = parse_protocol_list(orig); smartlist_t *elts = parse_protocol_list(orig);
tt_assert(elts); tt_assert(elts);
@ -61,7 +61,7 @@ test_protover_parse(void *arg)
e = smartlist_get(elts, 3); e = smartlist_get(elts, 3);
tt_str_op(e->name, OP_EQ, "Quux"); tt_str_op(e->name, OP_EQ, "Quux");
tt_int_op(smartlist_len(e->ranges), OP_EQ, 4); tt_int_op(smartlist_len(e->ranges), OP_EQ, 3);
{ {
r = smartlist_get(e->ranges, 0); r = smartlist_get(e->ranges, 0);
tt_int_op(r->low, OP_EQ, 9); tt_int_op(r->low, OP_EQ, 9);
@ -74,10 +74,6 @@ test_protover_parse(void *arg)
r = smartlist_get(e->ranges, 2); r = smartlist_get(e->ranges, 2);
tt_int_op(r->low, OP_EQ, 15); tt_int_op(r->low, OP_EQ, 15);
tt_int_op(r->high, OP_EQ, 16); tt_int_op(r->high, OP_EQ, 16);
r = smartlist_get(e->ranges, 3);
tt_int_op(r->low, OP_EQ, 900);
tt_int_op(r->high, OP_EQ, 900);
} }
re_encoded = encode_protocol_list(elts); re_encoded = encode_protocol_list(elts);
@ -149,14 +145,14 @@ test_protover_vote(void *arg)
tt_str_op(result, OP_EQ, ""); tt_str_op(result, OP_EQ, "");
tor_free(result); tor_free(result);
smartlist_add(lst, (void*) "Foo=1-10,500 Bar=1,3-7,8"); smartlist_add(lst, (void*) "Foo=1-10,63 Bar=1,3-7,8");
result = protover_compute_vote(lst, 1); result = protover_compute_vote(lst, 1);
tt_str_op(result, OP_EQ, "Bar=1,3-8 Foo=1-10,500"); tt_str_op(result, OP_EQ, "Bar=1,3-8 Foo=1-10,63");
tor_free(result); tor_free(result);
smartlist_add(lst, (void*) "Quux=123-456,78 Bar=2-6,8 Foo=9"); smartlist_add(lst, (void*) "Quux=12-45 Bar=2-6,8 Foo=9");
result = protover_compute_vote(lst, 1); result = protover_compute_vote(lst, 1);
tt_str_op(result, OP_EQ, "Bar=1-8 Foo=1-10,500 Quux=78,123-456"); tt_str_op(result, OP_EQ, "Bar=1-8 Foo=1-10,63 Quux=12-45");
tor_free(result); tor_free(result);
result = protover_compute_vote(lst, 2); result = protover_compute_vote(lst, 2);
@ -194,45 +190,16 @@ test_protover_vote(void *arg)
/* Just below the threshold: Rust */ /* Just below the threshold: Rust */
smartlist_clear(lst); smartlist_clear(lst);
smartlist_add(lst, (void*) "Sleen=1-500"); smartlist_add(lst, (void*) "Sleen=1-50");
result = protover_compute_vote(lst, 1); result = protover_compute_vote(lst, 1);
tt_str_op(result, OP_EQ, "Sleen=1-500"); tt_str_op(result, OP_EQ, "Sleen=1-50");
tor_free(result); tor_free(result);
/* Just below the threshold: C */ /* Just below the threshold: C */
smartlist_clear(lst); smartlist_clear(lst);
smartlist_add(lst, (void*) "Sleen=1-65536"); smartlist_add(lst, (void*) "Sleen=1-63");
result = protover_compute_vote(lst, 1); result = protover_compute_vote(lst, 1);
tt_str_op(result, OP_EQ, "Sleen=1-65536"); tt_str_op(result, OP_EQ, "Sleen=1-63");
tor_free(result);
/* Large protover lists that exceed the threshold */
/* By adding two votes, C allows us to exceed the limit */
smartlist_add(lst, (void*) "Sleen=1-65536");
smartlist_add(lst, (void*) "Sleen=100000");
result = protover_compute_vote(lst, 1);
tt_str_op(result, OP_EQ, "Sleen=1-65536,100000");
tor_free(result);
/* Large integers */
smartlist_clear(lst);
smartlist_add(lst, (void*) "Sleen=4294967294");
result = protover_compute_vote(lst, 1);
tt_str_op(result, OP_EQ, "Sleen=4294967294");
tor_free(result);
/* This parses, but fails at the vote stage */
smartlist_clear(lst);
smartlist_add(lst, (void*) "Sleen=4294967295");
result = protover_compute_vote(lst, 1);
tt_str_op(result, OP_EQ, "");
tor_free(result);
smartlist_clear(lst);
smartlist_add(lst, (void*) "Sleen=4294967296");
result = protover_compute_vote(lst, 1);
tt_str_op(result, OP_EQ, "");
tor_free(result); tor_free(result);
/* Protocol name too long */ /* Protocol name too long */
@ -272,8 +239,8 @@ test_protover_all_supported(void *arg)
tt_assert(! protover_all_supported("Wombat=9", &msg)); tt_assert(! protover_all_supported("Wombat=9", &msg));
tt_str_op(msg, OP_EQ, "Wombat=9"); tt_str_op(msg, OP_EQ, "Wombat=9");
tor_free(msg); tor_free(msg);
tt_assert(! protover_all_supported("Link=999", &msg)); tt_assert(! protover_all_supported("Link=60", &msg));
tt_str_op(msg, OP_EQ, "Link=999"); tt_str_op(msg, OP_EQ, "Link=60");
tor_free(msg); tor_free(msg);
// Mix of things we support and things we don't // Mix of things we support and things we don't
@ -283,11 +250,11 @@ test_protover_all_supported(void *arg)
/* Mix of things we support and don't support within a single protocol /* Mix of things we support and don't support within a single protocol
* which we do support */ * which we do support */
tt_assert(! protover_all_supported("Link=3-999", &msg)); tt_assert(! protover_all_supported("Link=3-60", &msg));
tt_str_op(msg, OP_EQ, "Link=6-999"); tt_str_op(msg, OP_EQ, "Link=6-60");
tor_free(msg); tor_free(msg);
tt_assert(! protover_all_supported("Link=1-3,345-666", &msg)); tt_assert(! protover_all_supported("Link=1-3,50-63", &msg));
tt_str_op(msg, OP_EQ, "Link=345-666"); tt_str_op(msg, OP_EQ, "Link=50-63");
tor_free(msg); tor_free(msg);
tt_assert(! protover_all_supported("Link=1-3,5-12", &msg)); tt_assert(! protover_all_supported("Link=1-3,5-12", &msg));
tt_str_op(msg, OP_EQ, "Link=6-12"); tt_str_op(msg, OP_EQ, "Link=6-12");
@ -295,18 +262,8 @@ test_protover_all_supported(void *arg)
/* Mix of protocols we do support and some we don't, where the protocols /* Mix of protocols we do support and some we don't, where the protocols
* we do support have some versions we don't support. */ * we do support have some versions we don't support. */
tt_assert(! protover_all_supported("Link=1-3,5-12 Quokka=9000-9001", &msg)); tt_assert(! protover_all_supported("Link=1-3,5-12 Quokka=40-41", &msg));
tt_str_op(msg, OP_EQ, "Link=6-12 Quokka=9000-9001"); tt_str_op(msg, OP_EQ, "Link=6-12 Quokka=40-41");
tor_free(msg);
/* We shouldn't be able to DoS ourselves parsing a large range. */
tt_assert(! protover_all_supported("Sleen=1-2147483648", &msg));
tt_str_op(msg, OP_EQ, "Sleen=1-2147483648");
tor_free(msg);
/* This case is allowed. */
tt_assert(! protover_all_supported("Sleen=1-4294967294", &msg));
tt_str_op(msg, OP_EQ, "Sleen=1-4294967294");
tor_free(msg); tor_free(msg);
/* If we get a (barely) valid (but unsupported list, we say "yes, that's /* If we get a (barely) valid (but unsupported list, we say "yes, that's
@ -566,9 +523,9 @@ test_protover_vote_roundtrip(void *args)
/* Will fail because of 4294967295. */ /* Will fail because of 4294967295. */
{ "Foo=1,3 Bar=3 Baz= Quux=9-12,14,15-16,900 Zn=1,4294967295", { "Foo=1,3 Bar=3 Baz= Quux=9-12,14,15-16,900 Zn=1,4294967295",
NULL }, NULL },
{ "Foo=1,3 Bar=3 Baz= Quux=9-12,14,15-16,900 Zn=1,4294967294", { "Foo=1,3 Bar=3 Baz= Quux=9-12,14,15-16,50 Zn=1,42",
"Bar=3 Foo=1,3 Quux=9-12,14-16,900 Zn=1,4294967294" }, "Bar=3 Foo=1,3 Quux=9-12,14-16,50 Zn=1,42" },
{ "Zu16=1,65536", "Zu16=1,65536" }, { "Zu16=1,63", "Zu16=1,63" },
{ "N-1=1,2", "N-1=1-2" }, { "N-1=1,2", "N-1=1-2" },
{ "-1=4294967295", NULL }, { "-1=4294967295", NULL },
{ "-1=3", "-1=3" }, { "-1=3", "-1=3" },
@ -602,12 +559,8 @@ test_protover_vote_roundtrip(void *args)
/* Large integers */ /* Large integers */
{ "Link=4294967296", NULL }, { "Link=4294967296", NULL },
/* Large range */ /* Large range */
{ "Sleen=1-501", "Sleen=1-501" }, { "Sleen=1-63", "Sleen=1-63" },
{ "Sleen=1-65537", NULL }, { "Sleen=1-65537", NULL },
/* Both C/Rust implementations should be able to handle this mild DoS. */
{ "Sleen=1-2147483648", NULL },
/* Rust tests are built in debug mode, so ints are bounds-checked. */
{ "Sleen=1-4294967295", NULL },
}; };
unsigned u; unsigned u;
smartlist_t *votes = smartlist_new(); smartlist_t *votes = smartlist_new();