mirror of
https://github.com/fafhrd91/actix-web
synced 2024-12-04 04:11:56 +01:00
fix soundness concern in h1 decoder (#1614)
Co-authored-by: Yuki Okushi <huyuumi.dev@gmail.com>
This commit is contained in:
parent
0ec335a39c
commit
6dc47c4093
@ -1,6 +1,11 @@
|
|||||||
# Changes
|
# Changes
|
||||||
|
|
||||||
## [Unreleased] - xxx
|
## [Unreleased] - xxx
|
||||||
|
### Fixed
|
||||||
|
* Potential UB in h1 decoder using uninitialized memory. [#1614]
|
||||||
|
|
||||||
|
[#1614]: https://github.com/actix/actix-web/pull/1614
|
||||||
|
|
||||||
|
|
||||||
## [2.0.0-beta.1] - 2020-07-11
|
## [2.0.0-beta.1] - 2020-07-11
|
||||||
|
|
||||||
|
@ -103,3 +103,7 @@ harness = false
|
|||||||
[[bench]]
|
[[bench]]
|
||||||
name = "status-line"
|
name = "status-line"
|
||||||
harness = false
|
harness = false
|
||||||
|
|
||||||
|
[[bench]]
|
||||||
|
name = "uninit-headers"
|
||||||
|
harness = false
|
||||||
|
137
actix-http/benches/uninit-headers.rs
Normal file
137
actix-http/benches/uninit-headers.rs
Normal file
@ -0,0 +1,137 @@
|
|||||||
|
use criterion::{criterion_group, criterion_main, Criterion};
|
||||||
|
|
||||||
|
use bytes::BytesMut;
|
||||||
|
|
||||||
|
// A Miri run detects UB, seen on this playground:
|
||||||
|
// https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=f5d9aa166aa48df8dca05fce2b6c3915
|
||||||
|
|
||||||
|
fn bench_header_parsing(c: &mut Criterion) {
|
||||||
|
c.bench_function("Original (Unsound) [short]", |b| {
|
||||||
|
b.iter(|| {
|
||||||
|
let mut buf = BytesMut::from(REQ_SHORT);
|
||||||
|
_original::parse_headers(&mut buf);
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
c.bench_function("New (safe) [short]", |b| {
|
||||||
|
b.iter(|| {
|
||||||
|
let mut buf = BytesMut::from(REQ_SHORT);
|
||||||
|
_new::parse_headers(&mut buf);
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
c.bench_function("Original (Unsound) [realistic]", |b| {
|
||||||
|
b.iter(|| {
|
||||||
|
let mut buf = BytesMut::from(REQ);
|
||||||
|
_original::parse_headers(&mut buf);
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
c.bench_function("New (safe) [realistic]", |b| {
|
||||||
|
b.iter(|| {
|
||||||
|
let mut buf = BytesMut::from(REQ);
|
||||||
|
_new::parse_headers(&mut buf);
|
||||||
|
})
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
criterion_group!(benches, bench_header_parsing);
|
||||||
|
criterion_main!(benches);
|
||||||
|
|
||||||
|
const MAX_HEADERS: usize = 96;
|
||||||
|
|
||||||
|
const EMPTY_HEADER_ARRAY: [httparse::Header<'static>; MAX_HEADERS] =
|
||||||
|
[httparse::EMPTY_HEADER; MAX_HEADERS];
|
||||||
|
|
||||||
|
#[derive(Clone, Copy)]
|
||||||
|
struct HeaderIndex {
|
||||||
|
name: (usize, usize),
|
||||||
|
value: (usize, usize),
|
||||||
|
}
|
||||||
|
|
||||||
|
const EMPTY_HEADER_INDEX: HeaderIndex = HeaderIndex {
|
||||||
|
name: (0, 0),
|
||||||
|
value: (0, 0),
|
||||||
|
};
|
||||||
|
|
||||||
|
const EMPTY_HEADER_INDEX_ARRAY: [HeaderIndex; MAX_HEADERS] =
|
||||||
|
[EMPTY_HEADER_INDEX; MAX_HEADERS];
|
||||||
|
|
||||||
|
impl HeaderIndex {
|
||||||
|
fn record(
|
||||||
|
bytes: &[u8],
|
||||||
|
headers: &[httparse::Header<'_>],
|
||||||
|
indices: &mut [HeaderIndex],
|
||||||
|
) {
|
||||||
|
let bytes_ptr = bytes.as_ptr() as usize;
|
||||||
|
for (header, indices) in headers.iter().zip(indices.iter_mut()) {
|
||||||
|
let name_start = header.name.as_ptr() as usize - bytes_ptr;
|
||||||
|
let name_end = name_start + header.name.len();
|
||||||
|
indices.name = (name_start, name_end);
|
||||||
|
let value_start = header.value.as_ptr() as usize - bytes_ptr;
|
||||||
|
let value_end = value_start + header.value.len();
|
||||||
|
indices.value = (value_start, value_end);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// test cases taken from:
|
||||||
|
// https://github.com/seanmonstar/httparse/blob/master/benches/parse.rs
|
||||||
|
|
||||||
|
const REQ_SHORT: &'static [u8] = b"\
|
||||||
|
GET / HTTP/1.0\r\n\
|
||||||
|
Host: example.com\r\n\
|
||||||
|
Cookie: session=60; user_id=1\r\n\r\n";
|
||||||
|
|
||||||
|
const REQ: &'static [u8] = b"\
|
||||||
|
GET /wp-content/uploads/2010/03/hello-kitty-darth-vader-pink.jpg HTTP/1.1\r\n\
|
||||||
|
Host: www.kittyhell.com\r\n\
|
||||||
|
User-Agent: Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.6; ja-JP-mac; rv:1.9.2.3) Gecko/20100401 Firefox/3.6.3 Pathtraq/0.9\r\n\
|
||||||
|
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\r\n\
|
||||||
|
Accept-Language: ja,en-us;q=0.7,en;q=0.3\r\n\
|
||||||
|
Accept-Encoding: gzip,deflate\r\n\
|
||||||
|
Accept-Charset: Shift_JIS,utf-8;q=0.7,*;q=0.7\r\n\
|
||||||
|
Keep-Alive: 115\r\n\
|
||||||
|
Connection: keep-alive\r\n\
|
||||||
|
Cookie: wp_ozh_wsa_visits=2; wp_ozh_wsa_visit_lasttime=xxxxxxxxxx; __utma=xxxxxxxxx.xxxxxxxxxx.xxxxxxxxxx.xxxxxxxxxx.xxxxxxxxxx.x; __utmz=xxxxxxxxx.xxxxxxxxxx.x.x.utmccn=(referral)|utmcsr=reader.livedoor.com|utmcct=/reader/|utmcmd=referral|padding=under256\r\n\r\n";
|
||||||
|
|
||||||
|
mod _new {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
pub fn parse_headers(src: &mut BytesMut) -> usize {
|
||||||
|
let mut headers: [HeaderIndex; MAX_HEADERS] = EMPTY_HEADER_INDEX_ARRAY;
|
||||||
|
let mut parsed: [httparse::Header<'_>; MAX_HEADERS] = EMPTY_HEADER_ARRAY;
|
||||||
|
|
||||||
|
let mut req = httparse::Request::new(&mut parsed);
|
||||||
|
match req.parse(src).unwrap() {
|
||||||
|
httparse::Status::Complete(_len) => {
|
||||||
|
HeaderIndex::record(src, req.headers, &mut headers);
|
||||||
|
req.headers.len()
|
||||||
|
}
|
||||||
|
_ => unreachable!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mod _original {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
use std::mem::MaybeUninit;
|
||||||
|
|
||||||
|
pub fn parse_headers(src: &mut BytesMut) -> usize {
|
||||||
|
let mut headers: [HeaderIndex; MAX_HEADERS] =
|
||||||
|
unsafe { MaybeUninit::uninit().assume_init() };
|
||||||
|
|
||||||
|
let mut parsed: [httparse::Header<'_>; MAX_HEADERS] =
|
||||||
|
unsafe { MaybeUninit::uninit().assume_init() };
|
||||||
|
|
||||||
|
let mut req = httparse::Request::new(&mut parsed);
|
||||||
|
match req.parse(src).unwrap() {
|
||||||
|
httparse::Status::Complete(_len) => {
|
||||||
|
HeaderIndex::record(src, req.headers, &mut headers);
|
||||||
|
req.headers.len()
|
||||||
|
}
|
||||||
|
_ => unreachable!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,7 +1,6 @@
|
|||||||
use std::convert::TryFrom;
|
use std::convert::TryFrom;
|
||||||
use std::io;
|
use std::io;
|
||||||
use std::marker::PhantomData;
|
use std::marker::PhantomData;
|
||||||
use std::mem::MaybeUninit;
|
|
||||||
use std::task::Poll;
|
use std::task::Poll;
|
||||||
|
|
||||||
use actix_codec::Decoder;
|
use actix_codec::Decoder;
|
||||||
@ -77,7 +76,7 @@ pub(crate) trait MessageType: Sized {
|
|||||||
let name =
|
let name =
|
||||||
HeaderName::from_bytes(&slice[idx.name.0..idx.name.1]).unwrap();
|
HeaderName::from_bytes(&slice[idx.name.0..idx.name.1]).unwrap();
|
||||||
|
|
||||||
// Unsafe: httparse check header value for valid utf-8
|
// SAFETY: httparse checks header value is valid UTF-8
|
||||||
let value = unsafe {
|
let value = unsafe {
|
||||||
HeaderValue::from_maybe_shared_unchecked(
|
HeaderValue::from_maybe_shared_unchecked(
|
||||||
slice.slice(idx.value.0..idx.value.1),
|
slice.slice(idx.value.0..idx.value.1),
|
||||||
@ -184,16 +183,11 @@ impl MessageType for Request {
|
|||||||
&mut self.head_mut().headers
|
&mut self.head_mut().headers
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::uninit_assumed_init)]
|
|
||||||
fn decode(src: &mut BytesMut) -> Result<Option<(Self, PayloadType)>, ParseError> {
|
fn decode(src: &mut BytesMut) -> Result<Option<(Self, PayloadType)>, ParseError> {
|
||||||
// Unsafe: we read only this data only after httparse parses headers into.
|
let mut headers: [HeaderIndex; MAX_HEADERS] = EMPTY_HEADER_INDEX_ARRAY;
|
||||||
// performance bump for pipeline benchmarks.
|
|
||||||
let mut headers: [HeaderIndex; MAX_HEADERS] =
|
|
||||||
unsafe { MaybeUninit::uninit().assume_init() };
|
|
||||||
|
|
||||||
let (len, method, uri, ver, h_len) = {
|
let (len, method, uri, ver, h_len) = {
|
||||||
let mut parsed: [httparse::Header<'_>; MAX_HEADERS] =
|
let mut parsed: [httparse::Header<'_>; MAX_HEADERS] = EMPTY_HEADER_ARRAY;
|
||||||
unsafe { MaybeUninit::uninit().assume_init() };
|
|
||||||
|
|
||||||
let mut req = httparse::Request::new(&mut parsed);
|
let mut req = httparse::Request::new(&mut parsed);
|
||||||
match req.parse(src)? {
|
match req.parse(src)? {
|
||||||
@ -260,16 +254,11 @@ impl MessageType for ResponseHead {
|
|||||||
&mut self.headers
|
&mut self.headers
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::uninit_assumed_init)]
|
|
||||||
fn decode(src: &mut BytesMut) -> Result<Option<(Self, PayloadType)>, ParseError> {
|
fn decode(src: &mut BytesMut) -> Result<Option<(Self, PayloadType)>, ParseError> {
|
||||||
// Unsafe: we read only this data only after httparse parses headers into.
|
let mut headers: [HeaderIndex; MAX_HEADERS] = EMPTY_HEADER_INDEX_ARRAY;
|
||||||
// performance bump for pipeline benchmarks.
|
|
||||||
let mut headers: [HeaderIndex; MAX_HEADERS] =
|
|
||||||
unsafe { MaybeUninit::uninit().assume_init() };
|
|
||||||
|
|
||||||
let (len, ver, status, h_len) = {
|
let (len, ver, status, h_len) = {
|
||||||
let mut parsed: [httparse::Header<'_>; MAX_HEADERS] =
|
let mut parsed: [httparse::Header<'_>; MAX_HEADERS] = EMPTY_HEADER_ARRAY;
|
||||||
unsafe { MaybeUninit::uninit().assume_init() };
|
|
||||||
|
|
||||||
let mut res = httparse::Response::new(&mut parsed);
|
let mut res = httparse::Response::new(&mut parsed);
|
||||||
match res.parse(src)? {
|
match res.parse(src)? {
|
||||||
@ -324,6 +313,17 @@ pub(crate) struct HeaderIndex {
|
|||||||
pub(crate) value: (usize, usize),
|
pub(crate) value: (usize, usize),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) const EMPTY_HEADER_INDEX: HeaderIndex = HeaderIndex {
|
||||||
|
name: (0, 0),
|
||||||
|
value: (0, 0),
|
||||||
|
};
|
||||||
|
|
||||||
|
pub(crate) const EMPTY_HEADER_INDEX_ARRAY: [HeaderIndex; MAX_HEADERS] =
|
||||||
|
[EMPTY_HEADER_INDEX; MAX_HEADERS];
|
||||||
|
|
||||||
|
pub(crate) const EMPTY_HEADER_ARRAY: [httparse::Header<'static>; MAX_HEADERS] =
|
||||||
|
[httparse::EMPTY_HEADER; MAX_HEADERS];
|
||||||
|
|
||||||
impl HeaderIndex {
|
impl HeaderIndex {
|
||||||
pub(crate) fn record(
|
pub(crate) fn record(
|
||||||
bytes: &[u8],
|
bytes: &[u8],
|
||||||
@ -973,7 +973,7 @@ mod tests {
|
|||||||
unreachable!("Error");
|
unreachable!("Error");
|
||||||
}
|
}
|
||||||
|
|
||||||
// type in chunked
|
// intentional typo in "chunked"
|
||||||
let mut buf = BytesMut::from(
|
let mut buf = BytesMut::from(
|
||||||
"GET /test HTTP/1.1\r\n\
|
"GET /test HTTP/1.1\r\n\
|
||||||
transfer-encoding: chnked\r\n\r\n",
|
transfer-encoding: chnked\r\n\r\n",
|
||||||
|
Loading…
Reference in New Issue
Block a user