#![allow(unsafe_code)]
use std::{
alloc::{GlobalAlloc, Layout, System},
sync::{
Mutex,
atomic::{AtomicPtr, AtomicUsize, Ordering},
},
};
use http_net::{
ClientConn, ClientEvent, FieldSpan, Limits, OwnedRequestHead, Parsed, Request, Response,
ServerConn, ServerEvent, parse_request_head,
};
static MARKER_PTR: AtomicPtr<u8> = AtomicPtr::new(std::ptr::null_mut());
static MARKER_LEN: AtomicUsize = AtomicUsize::new(0);
static LEAKED: AtomicUsize = AtomicUsize::new(0);
static SERIAL: Mutex<()> = Mutex::new(());
struct Inspecting;
unsafe impl GlobalAlloc for Inspecting {
unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
unsafe { System.alloc(layout) }
}
unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {
let len = MARKER_LEN.load(Ordering::Acquire);
if len > 0 && layout.size() >= len {
let marker =
unsafe { std::slice::from_raw_parts(MARKER_PTR.load(Ordering::Acquire), len) };
let block = unsafe { std::slice::from_raw_parts(ptr, layout.size()) };
if block.windows(len).any(|w| w == marker) {
let _ = LEAKED.fetch_add(1, Ordering::Relaxed);
}
}
unsafe { System.dealloc(ptr, layout) }
}
}
#[global_allocator]
static GLOBAL: Inspecting = Inspecting;
fn leaked_blocks(marker: &'static [u8], exchange: impl FnOnce()) -> usize {
let _serial = SERIAL.lock().unwrap_or_else(|e| e.into_inner());
LEAKED.store(0, Ordering::Relaxed);
MARKER_PTR.store(marker.as_ptr().cast_mut(), Ordering::Release);
MARKER_LEN.store(marker.len(), Ordering::Release);
exchange();
MARKER_LEN.store(0, Ordering::Release);
LEAKED.load(Ordering::Relaxed)
}
fn big_head(start: &[u8], marker: &[u8]) -> Vec<u8> {
let mut head = start.to_vec();
head.extend_from_slice(marker);
head.extend_from_slice(b"\r\n");
for i in 0..60 {
head.extend_from_slice(format!("x-pad-{i:02}: {}\r\n", "p".repeat(90)).as_bytes());
}
head
}
fn serve(wire: &[u8], wipe: bool) {
let mut conn = ServerConn::new();
conn.set_wipe(wipe);
let mut out = Vec::new();
for piece in wire.chunks(1000) {
assert_eq!(conn.feed(piece), piece.len());
loop {
match conn.poll().unwrap() {
ServerEvent::NeedInput => break,
ServerEvent::RequestEnd => conn.respond(&Response::new(204), &mut out).unwrap(),
_ => {}
}
}
}
}
#[test]
fn test_server_wipe_leaves_no_credential_in_freed_blocks_after_growth() {
const MARKER: &[u8] = b"Zm9vOnNlcnZlci1tYXJrZXI";
let mut wire = big_head(
b"GET / HTTP/1.1\r\nHost: a\r\nAuthorization: Basic ",
MARKER,
);
wire.extend_from_slice(b"\r\n");
assert!(wire.len() > 6000 && wire.len() < 8 * 1024, "{}", wire.len());
assert_eq!(leaked_blocks(MARKER, || serve(&wire, true)), 0);
assert!(leaked_blocks(MARKER, || serve(&wire, false)) > 0);
}
fn fetch(wire: &[u8], wipe: bool) {
let mut conn = ClientConn::new();
conn.set_wipe(wipe);
conn.send_request(&Request::get("/").header("host", "a"), &mut Vec::new())
.unwrap();
for piece in wire.chunks(1000) {
assert_eq!(conn.feed(piece), piece.len());
loop {
match conn.poll().unwrap() {
ClientEvent::NeedInput | ClientEvent::Idle => break,
_ => {}
}
}
}
}
#[test]
fn test_client_wipe_leaves_no_cookie_in_freed_blocks_after_growth() {
const MARKER: &[u8] = b"c2Vzc2lvbi1jbGllbnQtbWFya2Vy";
let mut wire = big_head(b"HTTP/1.1 200 OK\r\nSet-Cookie: sid=", MARKER);
wire.extend_from_slice(b"Content-Length: 0\r\n\r\n");
assert_eq!(leaked_blocks(MARKER, || fetch(&wire, true)), 0);
assert!(leaked_blocks(MARKER, || fetch(&wire, false)) > 0);
}
#[test]
fn test_owned_head_wipe_zeroes_its_bytes_and_spare_capacity() {
const MARKER: &[u8] = b"b3duZWQtaGVhZC1tYXJrZXI";
let mut wire = b"GET / HTTP/1.1\r\nHost: a\r\nAuthorization: Basic ".to_vec();
wire.extend_from_slice(MARKER);
wire.extend_from_slice(b"\r\n\r\n");
let finish = |head: OwnedRequestHead, wipe: bool| {
if wipe { head.wipe() } else { drop(head) }
};
let owned = |wipe: bool| {
let mut conn = ServerConn::new();
conn.set_wipe(true);
assert_eq!(conn.feed(&wire), wire.len());
let ServerEvent::Request(req) = conn.poll().unwrap() else {
panic!("no request")
};
finish(req.to_owned(), wipe);
};
assert_eq!(leaked_blocks(MARKER, || owned(true)), 0);
assert!(leaked_blocks(MARKER, || owned(false)) > 0);
let reused = |wipe: bool| {
let (mut bytes, mut spans) = (Vec::new(), Vec::new());
let mut slots = [FieldSpan::default(); 8];
let Ok(Parsed::Complete { head, .. }) =
parse_request_head(&wire, &Limits::new(), &mut slots)
else {
panic!("no head")
};
let _ = head.view().copy_into(&mut bytes, &mut spans);
let short = b"GET / HTTP/1.1\r\nHost: a\r\n\r\n";
let mut slots = [FieldSpan::default(); 8];
let Ok(Parsed::Complete { head, .. }) =
parse_request_head(short, &Limits::new(), &mut slots)
else {
panic!("no head")
};
let facts = head.view().copy_into(&mut bytes, &mut spans);
assert!(bytes.capacity() > bytes.len());
finish(
OwnedRequestHead::from_parts(bytes, spans, facts).unwrap(),
wipe,
);
};
assert_eq!(leaked_blocks(MARKER, || reused(true)), 0);
assert!(leaked_blocks(MARKER, || reused(false)) > 0);
}