Skip to content

Commit c790429

Browse files
committed
Bind request body admission to ownership
1 parent 6d5ad25 commit c790429

9 files changed

Lines changed: 233 additions & 46 deletions

File tree

crates/fluxheim-server/src/native_http1.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -261,7 +261,7 @@ where
261261
return Ok(());
262262
}
263263
};
264-
let body = match read_body(
264+
let mut body = match read_body(
265265
policy,
266266
request_body_timeout,
267267
&mut stream,
@@ -320,6 +320,7 @@ where
320320
}
321321
Err(error) => return Err(error),
322322
};
323+
body.attach_admission(request_body_reservation);
323324
let mut request = request;
324325
request.body = body;
325326
request_handler.prepare_request_context(&mut request);

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

Lines changed: 49 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ use std::ops::Deref;
33

44
use fluxheim_protocol::{Http1RequestTarget, Http1Version, http1_request_target};
55

6+
use crate::request_body_budget::NativeRequestBodyReservation;
7+
68
#[derive(Debug, Eq, PartialEq)]
79
pub struct NativeHttp1Request {
810
pub method: String,
@@ -19,48 +21,68 @@ pub struct NativeHttp1Request {
1921
pub trailers: Vec<(String, String)>,
2022
}
2123

22-
#[derive(Default, Eq, PartialEq)]
23-
pub struct NativeHttp1RequestBody(Vec<u8>);
24+
#[derive(Default)]
25+
pub struct NativeHttp1RequestBody {
26+
bytes: Vec<u8>,
27+
admission: Option<NativeRequestBodyReservation>,
28+
}
2429

2530
impl NativeHttp1RequestBody {
2631
#[must_use]
2732
pub const fn empty() -> Self {
28-
Self(Vec::new())
33+
Self {
34+
bytes: Vec::new(),
35+
admission: None,
36+
}
2937
}
3038

3139
#[must_use]
3240
pub const fn from_vec(body: Vec<u8>) -> Self {
33-
Self(body)
41+
Self {
42+
bytes: body,
43+
admission: None,
44+
}
45+
}
46+
47+
pub(crate) fn attach_admission(&mut self, admission: NativeRequestBodyReservation) {
48+
if self.admission.is_some() {
49+
log::error!(
50+
target: "fluxheim::security",
51+
"request body admission was attached more than once"
52+
);
53+
std::process::abort();
54+
}
55+
self.admission = Some(admission);
3456
}
3557

3658
pub(crate) fn extend_from_slice(
3759
&mut self,
3860
bytes: &[u8],
3961
) -> Result<(), fluxheim_protocol::Http1ParseError> {
4062
let required = self
41-
.0
63+
.bytes
4264
.len()
4365
.checked_add(bytes.len())
4466
.ok_or(fluxheim_protocol::Http1ParseError::BodyTooLarge)?;
4567
self.reserve_capacity(required)?;
46-
self.0.extend_from_slice(bytes);
68+
self.bytes.extend_from_slice(bytes);
4769
Ok(())
4870
}
4971

5072
pub(crate) fn reserve_capacity(
5173
&mut self,
5274
required: usize,
5375
) -> Result<(), fluxheim_protocol::Http1ParseError> {
54-
if required <= self.0.capacity() {
76+
if required <= self.bytes.capacity() {
5577
return Ok(());
5678
}
5779
let mut replacement = Vec::new();
5880
replacement
5981
.try_reserve_exact(required)
6082
.map_err(|_| fluxheim_protocol::Http1ParseError::BodyTooLarge)?;
61-
replacement.extend_from_slice(&self.0);
62-
sanitization::unsafe_wipe::volatile_sanitize_vec(&mut self.0);
63-
self.0 = replacement;
83+
replacement.extend_from_slice(&self.bytes);
84+
sanitization::unsafe_wipe::volatile_sanitize_vec(&mut self.bytes);
85+
self.bytes = replacement;
6486
Ok(())
6587
}
6688

@@ -72,22 +94,30 @@ impl NativeHttp1RequestBody {
7294
replacement
7395
.try_reserve_exact(required)
7496
.map_err(|_| fluxheim_protocol::Http1ParseError::BodyTooLarge)?;
75-
replacement.extend_from_slice(&self.0);
76-
sanitization::unsafe_wipe::volatile_sanitize_vec(&mut self.0);
77-
self.0 = replacement;
97+
replacement.extend_from_slice(&self.bytes);
98+
sanitization::unsafe_wipe::volatile_sanitize_vec(&mut self.bytes);
99+
self.bytes = replacement;
78100
Ok(())
79101
}
80102

81103
pub(crate) fn capacity(&self) -> usize {
82-
self.0.capacity()
104+
self.bytes.capacity()
105+
}
106+
}
107+
108+
impl PartialEq for NativeHttp1RequestBody {
109+
fn eq(&self, other: &Self) -> bool {
110+
self.bytes == other.bytes
83111
}
84112
}
85113

114+
impl Eq for NativeHttp1RequestBody {}
115+
86116
impl std::fmt::Debug for NativeHttp1RequestBody {
87117
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
88118
formatter
89119
.debug_struct("NativeHttp1RequestBody")
90-
.field("len", &self.0.len())
120+
.field("len", &self.bytes.len())
91121
.field("contents", &"<redacted>")
92122
.finish()
93123
}
@@ -97,19 +127,19 @@ impl Deref for NativeHttp1RequestBody {
97127
type Target = [u8];
98128

99129
fn deref(&self) -> &Self::Target {
100-
&self.0
130+
&self.bytes
101131
}
102132
}
103133

104134
impl AsRef<[u8]> for NativeHttp1RequestBody {
105135
fn as_ref(&self) -> &[u8] {
106-
&self.0
136+
&self.bytes
107137
}
108138
}
109139

110140
impl fluxheim_protocol::Http1ChunkSink for NativeHttp1RequestBody {
111141
fn len(&self) -> usize {
112-
self.0.len()
142+
self.bytes.len()
113143
}
114144

115145
fn append(&mut self, bytes: &[u8]) -> Result<(), fluxheim_protocol::Http1ParseError> {
@@ -119,7 +149,7 @@ impl fluxheim_protocol::Http1ChunkSink for NativeHttp1RequestBody {
119149

120150
impl Drop for NativeHttp1RequestBody {
121151
fn drop(&mut self) {
122-
sanitization::unsafe_wipe::volatile_sanitize_vec(&mut self.0);
152+
sanitization::unsafe_wipe::volatile_sanitize_vec(&mut self.bytes);
123153
}
124154
}
125155

crates/fluxheim-server/src/native_http1_tests.rs

Lines changed: 64 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,11 @@ struct PinningBodyBudgetHandler {
2222
pinned: Arc<HoldingBodyBudgetHandler>,
2323
}
2424

25+
struct RetainingBodyBudgetHandler {
26+
budget: NativeRequestBodyBudget,
27+
retained: std::sync::Mutex<Option<crate::NativeHttp1RequestBody>>,
28+
}
29+
2530
impl NativeHttp1Handler for PinningBodyBudgetHandler {
2631
fn pin_request_handler(&self) -> Option<Arc<dyn NativeHttp1Handler>> {
2732
Some(self.pinned.clone())
@@ -42,16 +47,33 @@ impl NativeHttp1Handler for HoldingBodyBudgetHandler {
4247

4348
fn handle<'a>(
4449
&'a self,
45-
_request: NativeHttp1Request,
50+
request: NativeHttp1Request,
4651
) -> std::pin::Pin<Box<dyn std::future::Future<Output = NativeHttp1Response> + Send + 'a>> {
4752
Box::pin(async move {
4853
self.entered.notify_one();
4954
self.release.notified().await;
55+
drop(request);
5056
NativeHttp1Response::new(200, "OK", b"ok\n").close_connection()
5157
})
5258
}
5359
}
5460

61+
impl NativeHttp1Handler for RetainingBodyBudgetHandler {
62+
fn request_body_budget(&self) -> NativeRequestBodyBudget {
63+
self.budget.clone()
64+
}
65+
66+
fn handle<'a>(
67+
&'a self,
68+
request: NativeHttp1Request,
69+
) -> std::pin::Pin<Box<dyn std::future::Future<Output = NativeHttp1Response> + Send + 'a>> {
70+
Box::pin(async move {
71+
*self.retained.lock().unwrap() = Some(request.body);
72+
NativeHttp1Response::new(200, "OK", b"retained\n").close_connection()
73+
})
74+
}
75+
}
76+
5577
pub(crate) async fn spawn_server(
5678
handler: impl Fn(NativeHttp1Request) -> NativeHttp1Response + Send + Sync + 'static,
5779
) -> std::net::SocketAddr {
@@ -572,3 +594,44 @@ async fn native_http1_rejects_aggregate_request_body_overcommit() {
572594
);
573595
server.await.unwrap();
574596
}
597+
598+
#[tokio::test]
599+
async fn native_http1_body_retains_admission_after_handler_returns() {
600+
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
601+
let address = listener.local_addr().unwrap();
602+
let budget = NativeRequestBodyBudget::new(64 * 1024);
603+
let handler = Arc::new(RetainingBodyBudgetHandler {
604+
budget: budget.clone(),
605+
retained: std::sync::Mutex::new(None),
606+
});
607+
let server_handler = handler.clone();
608+
let server = tokio::spawn(async move {
609+
let (stream, peer) = listener.accept().await.unwrap();
610+
serve_native_http1_connection(
611+
stream,
612+
Some(peer),
613+
DownstreamHttp1Policy::default(),
614+
server_handler,
615+
)
616+
.await
617+
.unwrap();
618+
});
619+
let mut stream = TcpStream::connect(address).await.unwrap();
620+
621+
stream
622+
.write_all(
623+
b"POST /retain HTTP/1.1\r\nHost: local.test\r\nContent-Length: 1\r\nConnection: close\r\n\r\na",
624+
)
625+
.await
626+
.unwrap();
627+
assert!(
628+
read_response(&mut stream)
629+
.await
630+
.starts_with("HTTP/1.1 200 OK\r\n")
631+
);
632+
server.await.unwrap();
633+
634+
assert!(budget.reserve(1).await.is_err());
635+
handler.retained.lock().unwrap().take();
636+
assert!(budget.reserve(1).await.is_ok());
637+
}

crates/fluxheim-server/src/native_http2_client.rs

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -452,14 +452,20 @@ async fn drain_response_body(
452452
mod owned_body_tests {
453453
use super::*;
454454

455-
#[test]
456-
fn h2_bytes_retains_sanitizing_request_body_allocation() {
457-
let body = NativeHttp1RequestBody::from_vec(b"owned request body".to_vec());
455+
#[tokio::test]
456+
async fn h2_bytes_retains_body_allocation_and_admission() {
457+
let budget = crate::NativeRequestBodyBudget::new(64 * 1024);
458+
let admission = budget.reserve(1).await.unwrap();
459+
let mut body = NativeHttp1RequestBody::from_vec(b"owned request body".to_vec());
460+
body.attach_admission(admission);
458461
let original_pointer = body.as_ref().as_ptr();
459462

460463
let bytes = owned_request_body_bytes(body);
461464

462465
assert_eq!(bytes.as_ptr(), original_pointer);
463466
assert_eq!(bytes.as_ref(), b"owned request body");
467+
assert!(budget.reserve(1).await.is_err());
468+
drop(bytes);
469+
assert!(budget.reserve(1).await.is_ok());
464470
}
465471
}

crates/fluxheim-server/src/native_http2_route_adapter.rs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ fn native_http2_request_to_http1(
7979
method,
8080
uri,
8181
headers,
82-
mut body,
82+
body,
8383
trailers,
8484
} = request;
8585
let authority = uri.authority()?.as_str().to_owned();
@@ -107,7 +107,7 @@ fn native_http2_request_to_http1(
107107
target,
108108
version: fluxheim_protocol::Http1Version::Http11,
109109
headers: owned_headers,
110-
body: crate::NativeHttp1RequestBody::from_vec(std::mem::take(&mut *body)),
110+
body,
111111
trailers: native_http2_headers_to_native(trailers.as_ref())?,
112112
})
113113
}
@@ -160,7 +160,7 @@ mod tests {
160160
method: http::Method::POST,
161161
uri: "https://native.test/upload?x=1".parse().unwrap(),
162162
headers,
163-
body: zeroize::Zeroizing::new(b"body".to_vec()),
163+
body: crate::NativeHttp1RequestBody::from_vec(b"body".to_vec()),
164164
trailers: Some({
165165
let mut trailers = http::HeaderMap::new();
166166
trailers.insert("grpc-status", http::HeaderValue::from_static("0"));
@@ -224,7 +224,7 @@ mod tests {
224224
method: http::Method::GET,
225225
uri: "https://public.test/resource".parse().unwrap(),
226226
headers,
227-
body: zeroize::Zeroizing::new(Vec::new()),
227+
body: crate::NativeHttp1RequestBody::empty(),
228228
trailers: None,
229229
};
230230

@@ -247,7 +247,7 @@ mod tests {
247247
method: http::Method::GET,
248248
uri: "/resource".parse().unwrap(),
249249
headers: http::HeaderMap::new(),
250-
body: zeroize::Zeroizing::new(Vec::new()),
250+
body: crate::NativeHttp1RequestBody::empty(),
251251
trailers: None,
252252
};
253253

0 commit comments

Comments
 (0)