Skip to content

Commit 0fcfff4

Browse files
committed
Harden native request body ownership
1 parent fcc5b38 commit 0fcfff4

38 files changed

Lines changed: 444 additions & 87 deletions

Cargo.lock

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/fluxheim-protocol/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,4 @@ publish = false
88
description = "Internal Fluxheim protocol framing helpers."
99

1010
[dependencies]
11+
sanitization = { version = "1.2.4", features = ["alloc"] }

crates/fluxheim-protocol/src/http1_chunked.rs

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ enum Http1ChunkedDecodeState {
5454
End,
5555
}
5656

57-
#[derive(Clone, Debug, Eq, PartialEq)]
57+
#[derive(Debug, Eq, PartialEq)]
5858
pub struct Http1ChunkedDecoder {
5959
limits: Http1ChunkLimits,
6060
input: Vec<u8>,
@@ -97,6 +97,17 @@ impl Http1ChunkedDecoder {
9797
self.push_with_sink(chunk, output)
9898
}
9999

100+
pub fn push_to<S: Http1ChunkSink>(
101+
&mut self,
102+
chunk: &[u8],
103+
output: &mut S,
104+
) -> Result<Option<Http1ChunkedDecode>, Http1ParseError> {
105+
if output.len() != self.decoded_len {
106+
return Err(Http1ParseError::InvalidChunk);
107+
}
108+
self.push_with_sink(chunk, output)
109+
}
110+
100111
pub fn buffered_len(&self) -> usize {
101112
self.input.len()
102113
}
@@ -272,11 +283,27 @@ impl Http1ChunkedDecoder {
272283
}
273284
}
274285

275-
trait Http1ChunkSink {
286+
impl Drop for Http1ChunkedDecoder {
287+
fn drop(&mut self) {
288+
sanitization::unsafe_wipe::volatile_sanitize_vec(&mut self.input);
289+
}
290+
}
291+
292+
pub trait Http1ChunkSink {
293+
fn len(&self) -> usize;
294+
295+
fn is_empty(&self) -> bool {
296+
self.len() == 0
297+
}
298+
276299
fn append(&mut self, bytes: &[u8]) -> Result<(), Http1ParseError>;
277300
}
278301

279302
impl Http1ChunkSink for Vec<u8> {
303+
fn len(&self) -> usize {
304+
Vec::len(self)
305+
}
306+
280307
fn append(&mut self, bytes: &[u8]) -> Result<(), Http1ParseError> {
281308
self.try_reserve(bytes.len())
282309
.map_err(|_| Http1ParseError::BodyTooLarge)?;
@@ -291,6 +318,10 @@ struct Http1SliceChunkSink<'a> {
291318
}
292319

293320
impl Http1ChunkSink for Http1SliceChunkSink<'_> {
321+
fn len(&self) -> usize {
322+
self.len
323+
}
324+
294325
fn append(&mut self, bytes: &[u8]) -> Result<(), Http1ParseError> {
295326
let end = self
296327
.len

crates/fluxheim-protocol/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ pub use http1_chunked::{
2929
DEFAULT_HTTP1_MAX_BODY_BYTES, DEFAULT_HTTP1_MAX_CHUNK_COUNT,
3030
DEFAULT_HTTP1_MAX_CHUNK_EXTENSION_BYTES, DEFAULT_HTTP1_MAX_CHUNK_LINE_BYTES,
3131
DEFAULT_HTTP1_MAX_CHUNK_SIZE, DEFAULT_HTTP1_MAX_ENCODED_BYTES, Http1ChunkLimits,
32-
Http1ChunkedDecode, Http1ChunkedDecoder, decode_http1_chunked_body,
32+
Http1ChunkSink, Http1ChunkedDecode, Http1ChunkedDecoder, decode_http1_chunked_body,
3333
};
3434
pub use http1_connection::{
3535
Http1ConnectionDirective, Http1ConnectionOptions, http1_connection_directive,

crates/fluxheim-server/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,7 @@ pub use native_http1::serve_native_http1_rustls_listener;
119119
pub use native_http1::serve_native_http1_unix_listener;
120120
pub use native_http1::{
121121
NativeHttp1ConnectionStream, NativeHttp1Error, NativeHttp1GeoContext, NativeHttp1Handler,
122-
NativeHttp1Request, NativeHttp1RequestContext, NativeHttp1Response,
122+
NativeHttp1Request, NativeHttp1RequestBody, NativeHttp1RequestContext, NativeHttp1Response,
123123
NativeHttp1ResponseWritePolicy, NativeHttp1TlsClientIdentity, serve_native_http1_connection,
124124
serve_native_http1_listener, serve_native_http1_listener_with_proxy_protocol,
125125
};

crates/fluxheim-server/src/native_http1.rs

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ use fluxheim_protocol::{
1010
};
1111
use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite};
1212
use tokio::time::timeout;
13-
use zeroize::Zeroizing;
1413

1514
#[cfg(any(feature = "tls-rustls-backend", feature = "tls-openssl-backend"))]
1615
use crate::DownstreamHttp2Policy;
@@ -38,7 +37,7 @@ pub use openssl_listener::{
3837
};
3938
use proxy_protocol::read_proxy_protocol_source;
4039
pub use request::{
41-
NativeHttp1GeoContext, NativeHttp1Request, NativeHttp1RequestContext,
40+
NativeHttp1GeoContext, NativeHttp1Request, NativeHttp1RequestBody, NativeHttp1RequestContext,
4241
NativeHttp1TlsClientIdentity,
4342
};
4443
use response::write_response;
@@ -322,7 +321,7 @@ where
322321
Err(error) => return Err(error),
323322
};
324323
let mut request = request;
325-
request.body = Zeroizing::new(body);
324+
request.body = body;
326325
request_handler.prepare_request_context(&mut request);
327326
if request_handler.handles_connection_takeover(&request) {
328327
let prebuffered = std::mem::take(&mut buffer);
@@ -445,7 +444,7 @@ fn owned_request_from_head(
445444
target: head.target().to_owned(),
446445
version: head.version(),
447446
headers: owned_headers(head.headers(), head.effective_authority()),
448-
body: Zeroizing::new(Vec::new()),
447+
body: NativeHttp1RequestBody::empty(),
449448
trailers: Vec::new(),
450449
}
451450
}

crates/fluxheim-server/src/native_http1/body.rs

Lines changed: 87 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ use tokio::io::{AsyncRead, AsyncReadExt};
55
use tokio::time::timeout;
66

77
use crate::request_body_budget::NativeRequestBodyReservation;
8-
use crate::{DownstreamHttp1Policy, NativeHttp1Error};
8+
use crate::{DownstreamHttp1Policy, NativeHttp1Error, NativeHttp1RequestBody};
99

1010
const READ_CHUNK_BYTES: usize = 8192;
1111

@@ -17,7 +17,7 @@ pub(super) async fn read_body<S>(
1717
head_len: usize,
1818
framing: Http1BodyFraming,
1919
reservation: &mut NativeRequestBodyReservation,
20-
) -> Result<Vec<u8>, NativeHttp1Error>
20+
) -> Result<NativeHttp1RequestBody, NativeHttp1Error>
2121
where
2222
S: AsyncRead + Unpin,
2323
{
@@ -48,33 +48,45 @@ async fn read_body_inner<S>(
4848
framing: Http1BodyFraming,
4949
max_body_bytes: usize,
5050
reservation: &mut NativeRequestBodyReservation,
51-
) -> Result<Vec<u8>, NativeHttp1Error>
51+
) -> Result<NativeHttp1RequestBody, NativeHttp1Error>
5252
where
5353
S: AsyncRead + Unpin,
5454
{
5555
match framing {
5656
Http1BodyFraming::NoBody => {
57+
sanitization::unsafe_wipe::volatile_sanitize_bytes(&mut buffer[..head_len]);
5758
buffer.drain(..head_len);
58-
Ok(Vec::new())
59+
Ok(NativeHttp1RequestBody::empty())
5960
}
6061
Http1BodyFraming::ContentLength(length) => {
6162
let length = usize::try_from(length).map_err(|_| Http1ParseError::BodyTooLarge)?;
6263
if length > max_body_bytes {
6364
return Err(Http1ParseError::BodyTooLarge.into());
6465
}
65-
let required = head_len
66-
.checked_add(length)
66+
let available = buffer.len().saturating_sub(head_len).min(length);
67+
let body_end = head_len
68+
.checked_add(available)
6769
.ok_or(Http1ParseError::BodyTooLarge)?;
68-
while buffer.len() < required {
70+
let mut initial = std::mem::take(buffer);
71+
let remainder = initial.split_off(body_end);
72+
sanitization::unsafe_wipe::volatile_sanitize_bytes(&mut initial[..head_len]);
73+
initial.drain(..head_len);
74+
*buffer = remainder;
75+
let mut body = NativeHttp1RequestBody::from_vec(initial);
76+
body.replace_capacity(length)?;
77+
while body.len() < length {
78+
let remaining = length - body.len();
6979
let mut chunk = [0u8; READ_CHUNK_BYTES];
70-
let read = stream.read(&mut chunk).await?;
80+
let read = stream
81+
.read(&mut chunk[..remaining.min(READ_CHUNK_BYTES)])
82+
.await?;
7183
if read == 0 {
7284
return Err(Http1ParseError::InvalidContentLength.into());
7385
}
74-
buffer.extend_from_slice(&chunk[..read]);
86+
let append_result = body.extend_from_slice(&chunk[..read]);
87+
sanitization::unsafe_wipe::volatile_sanitize_bytes(&mut chunk[..read]);
88+
append_result?;
7589
}
76-
let body = buffer[head_len..required].to_vec();
77-
buffer.drain(..required);
7890
Ok(body)
7991
}
8092
Http1BodyFraming::Chunked => {
@@ -89,19 +101,17 @@ async fn read_chunked_body<S>(
89101
head_len: usize,
90102
max_body_bytes: usize,
91103
reservation: &mut NativeRequestBodyReservation,
92-
) -> Result<Vec<u8>, NativeHttp1Error>
104+
) -> Result<NativeHttp1RequestBody, NativeHttp1Error>
93105
where
94106
S: AsyncRead + Unpin,
95107
{
96108
buffer.drain(..head_len);
97109
let limits = Http1ChunkLimits::default().with_max_body_bytes(max_body_bytes);
98110
let mut decoder = Http1ChunkedDecoder::new(limits);
99-
let mut body = Vec::new();
100-
let initial = std::mem::take(buffer);
101-
reservation
102-
.grow_to(initial.len().min(max_body_bytes))
103-
.await?;
104-
if let Some(decoded) = decoder.push(&initial, &mut body)? {
111+
let mut body = NativeHttp1RequestBody::empty();
112+
let initial = NativeHttp1RequestBody::from_vec(std::mem::take(buffer));
113+
reserve_chunked_body_growth(&mut body, reservation, initial.len(), max_body_bytes).await?;
114+
if let Some(decoded) = decoder.push_to(&initial, &mut body)? {
105115
buffer.extend_from_slice(&initial[decoded.consumed_len..]);
106116
return Ok(body);
107117
}
@@ -115,10 +125,10 @@ where
115125
let prospective_encoded = fed_len
116126
.checked_add(read)
117127
.ok_or(Http1ParseError::EncodedBodyTooLarge)?;
118-
reservation
119-
.grow_to(prospective_encoded.min(max_body_bytes))
120-
.await?;
121-
if let Some(decoded) = decoder.push(&chunk[..read], &mut body)? {
128+
reserve_chunked_body_growth(&mut body, reservation, read, max_body_bytes).await?;
129+
let decoded = decoder.push_to(&chunk[..read], &mut body);
130+
sanitization::unsafe_wipe::volatile_sanitize_bytes(&mut chunk[..read]);
131+
if let Some(decoded) = decoded? {
122132
let consumed_from_chunk = decoded
123133
.consumed_len
124134
.checked_sub(fed_len)
@@ -132,3 +142,58 @@ where
132142
fed_len = prospective_encoded;
133143
}
134144
}
145+
146+
async fn reserve_chunked_body_growth(
147+
body: &mut NativeHttp1RequestBody,
148+
reservation: &mut NativeRequestBodyReservation,
149+
possible_growth: usize,
150+
max_body_bytes: usize,
151+
) -> Result<(), NativeHttp1Error> {
152+
let required = body
153+
.len()
154+
.checked_add(possible_growth)
155+
.ok_or(Http1ParseError::BodyTooLarge)?
156+
.min(max_body_bytes);
157+
if required <= body.capacity() {
158+
return Ok(());
159+
}
160+
let geometric = body.capacity().max(READ_CHUNK_BYTES).saturating_mul(2);
161+
let admitted_capacity = required.max(geometric).min(max_body_bytes);
162+
reservation.grow_to(admitted_capacity).await?;
163+
body.reserve_capacity(admitted_capacity)?;
164+
Ok(())
165+
}
166+
167+
#[cfg(test)]
168+
mod tests {
169+
use super::*;
170+
use crate::NativeRequestBodyBudget;
171+
172+
#[tokio::test]
173+
async fn content_length_moves_body_and_releases_connection_buffer_capacity() {
174+
let head = b"POST / HTTP/1.1\r\nHost: local.test\r\nContent-Length: 5\r\n\r\n";
175+
let pipeline = b"GET /next HTTP/1.1\r\nHost: local.test\r\n\r\n";
176+
let mut buffer = Vec::with_capacity(1024 * 1024);
177+
buffer.extend_from_slice(head);
178+
buffer.extend_from_slice(b"hello");
179+
buffer.extend_from_slice(pipeline);
180+
let original_capacity = buffer.capacity();
181+
let mut reservation = NativeRequestBodyBudget::new(1024).reserve(5).await.unwrap();
182+
183+
let body = read_body_inner(
184+
&mut tokio::io::empty(),
185+
&mut buffer,
186+
head.len(),
187+
Http1BodyFraming::ContentLength(5),
188+
1024,
189+
&mut reservation,
190+
)
191+
.await
192+
.unwrap();
193+
194+
assert_eq!(body.as_ref(), b"hello");
195+
assert_eq!(buffer, pipeline);
196+
assert!(buffer.capacity() < original_capacity);
197+
assert!(body.capacity() >= body.len());
198+
}
199+
}

0 commit comments

Comments
 (0)