From ad47ba9164d19fecf65995b77f1c180f56315648 Mon Sep 17 00:00:00 2001 From: "detail-app[bot]" <180357370+detail-app[bot]@users.noreply.github.com> Date: Fri, 16 Oct 2026 23:43:14 +0000 Subject: [PATCH] fix(http): reject impossible body length in elide_transport_http_respond before forming slice --- crates/bemo/src/abi/http.rs | 7 ++++- crates/bemo/tests/http.rs | 51 +++++++++++++++++++++++++++++++++++++ 2 files changed, 57 insertions(+), 1 deletion(-) diff --git a/crates/bemo/src/abi/http.rs b/crates/bemo/src/abi/http.rs index f5e5384..42e4c16 100644 --- a/crates/bemo/src/abi/http.rs +++ b/crates/bemo/src/abi/http.rs @@ -1543,7 +1543,12 @@ pub unsafe fn elide_transport_http_respond( _ if stream => &[], Ok(0) => &[], // SAFETY: The ABI caller guarantees length readable bytes for the duration of this call. - Ok(length) if !body.is_null() => unsafe { std::slice::from_raw_parts(body, length) }, + // No caller can ever make more than `isize::MAX` bytes readable, so reject impossible + // lengths (including the `u64::MAX` "unknown length" sentinel used only with streaming) + // before forming a slice that `slice::from_raw_parts` cannot soundly describe. + Ok(length) if length <= isize::MAX as usize && !body.is_null() => unsafe { + std::slice::from_raw_parts(body, length) + }, _ => return INVALID, }; let headers: Vec> = (0..count as usize) diff --git a/crates/bemo/tests/http.rs b/crates/bemo/tests/http.rs index 49d9a54..a5c48b0 100644 --- a/crates/bemo/tests/http.rs +++ b/crates/bemo/tests/http.rs @@ -3452,6 +3452,57 @@ fn http_calls_validate_arguments_before_touching_the_exchange() { h.finish(); } +#[test] +#[cfg_attr(miri, ignore = "io_uring and real sockets are unavailable under miri")] +fn respond_rejects_impossible_body_length_without_streaming_instead_of_ub() { + let mut h = harness(); + h.peer + .write_all(b"GET / HTTP/1.1\r\nHost: a\r\nConnection: close\r\n\r\n") + .unwrap(); + let exchange = h.take(EVENT_REQUEST, 1)[0].value; + let body = [0u8; 4]; + + // `u64::MAX` is the documented "unknown length" sentinel, but only alongside `RESPOND_STREAM`. + // Without the flag it must be rejected with `INVALID` rather than forming a slice larger than + // `isize::MAX`, which `slice::from_raw_parts` cannot soundly describe and no caller can ever + // satisfy the safety contract for. + assert_eq!( + // SAFETY: The exchange lease and body storage remain live; the oversized length is rejected before any read. + unsafe { elide_transport_http_respond(h.driver, exchange, 200, std::ptr::null(), 0, body.as_ptr(), u64::MAX, 0,) }, + INVALID, + "u64::MAX body length without RESPOND_STREAM must be rejected, not UB" + ); + // Any length above `isize::MAX` is equally impossible to satisfy and must be rejected the same way. + assert_eq!( + // SAFETY: The exchange lease and body storage remain live; the oversized length is rejected before any read. + unsafe { + elide_transport_http_respond( + h.driver, + exchange, + 200, + std::ptr::null(), + 0, + body.as_ptr(), + (isize::MAX as u64) + 1, + 0, + ) + }, + INVALID, + "body length above isize::MAX without RESPOND_STREAM must be rejected, not UB" + ); + + // The rejected calls must not have corrupted the exchange: a normal respond still works. + respond(h.driver, exchange, 200, &[], b"ok"); + h.take(EVENT_CLOSED, 1); + let mut got = Vec::new(); + h.peer.read_to_end(&mut got).unwrap(); + let text = String::from_utf8_lossy(&got); + assert!(text.starts_with("HTTP/1.1 200 OK\r\n"), "{text}"); + assert!(text.ends_with("\r\n\r\nok"), "{text}"); + assert_eq!(elide_transport_http_free(h.driver, exchange), 0); + h.finish(); +} + #[test] #[cfg_attr(miri, ignore = "io_uring and real sockets are unavailable under miri")] fn native_h2_streams_request_bodies_and_rejects_unsupported_expectations_natively() {