Watch
0
0
Fork
You've already forked hyperhive
0

bound the request line on the host admin and in-agent sockets

hive-c0re's admin socket and hive-agent's in-agent socket (mode 0666) read
the request with an unbounded read_line: no length cap and no deadline, so
a peer that never sends a newline grows a buffer and pins a task forever.

The bounded reader (16 MiB cap) moves from hive-c0re's socket_server into
hive-sock-client, the one crate both daemons already share or can take, and
gains optional deadlines for the first byte and for the rest of the line.
Both sockets now use it and answer an over-long or stalled line with the
same error reply the bounded sockets give, then close. The admin socket
bounds only the started line (clients may idle between requests); the
in-agent socket is single-shot and bounds silence too. The per-agent
sockets keep their behaviour (no deadline).
This commit is contained in:
atlas 2026-10-16 11:36:14 +02:00 • committed by mara
commit 1f214f9f01
10 changed files with 425 additions and 87 deletions

View file

@ -27,7 +27,8 @@ use std::sync::Arc;
use anyhow::{Context, Result};
use hive_agent_sock::{Request, Response};
use hive_sh4re::inbox::{LooseEnd, saturating_age};
use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
use hive_sock_client::{LineDeadlines, REQUEST_LINE_DEADLINE, RequestLine, read_request_line};
use tokio::io::{AsyncWriteExt, BufReader};
use tokio::net::{UnixListener, UnixStream};
use tokio::sync::Notify;
@ -146,18 +147,43 @@ async fn handle_conn(
wake: &Notify,
reminders: Option<&Reminders>,
bus: &Bus,
) -> Result<()> {
serve_one(stream, REQUEST_DEADLINES, |req| {
dispatch(req, store, wake, reminders, bus)
})
.await
}
/// One request line, one response line. A single-shot connection, so silence
/// is bounded too: the socket is mode 0666 and any local process can open it.
const REQUEST_DEADLINES: LineDeadlines = LineDeadlines {
idle: Some(REQUEST_LINE_DEADLINE),
line: Some(REQUEST_LINE_DEADLINE),
};
/// Read the request line under `deadlines`, answer it with `apply`, and write
/// the response. An over-long or stalled line gets a `Response::Err`.
async fn serve_one(
stream: UnixStream,
deadlines: LineDeadlines,
apply: impl FnOnce(Request) -> Response,
) -> Result<()> {
let (read, mut write) = stream.into_split();
let mut reader = BufReader::new(read);
let mut line = String::new();
if reader.read_line(&mut line).await? == 0 {
let mut line = Vec::new();
let outcome = read_request_line(&mut reader, &mut line, deadlines).await?;
if outcome == RequestLine::Eof {
return Ok(());
}
let resp = match serde_json::from_str::<Request>(line.trim()) {
Ok(req) => dispatch(req, store, wake, reminders, bus),
Err(e) => Response::Err {
message: format!("bad request: {e}"),
},
let resp = if let Some(message) = outcome.refusal() {
Response::Err { message }
} else {
match serde_json::from_slice::<Request>(line.trim_ascii()) {
Ok(req) => apply(req),
Err(e) => Response::Err {
message: format!("bad request: {e}"),
},
}
};
let mut out = serde_json::to_string(&resp)?;
out.push('\n');
@ -464,6 +490,8 @@ fn to_loose_end(t: Todo) -> LooseEnd {
mod tests {
use super::*;
use chrono::{DateTime, Duration, Utc};
use hive_sock_client::REQUEST_LINE_MAX_BYTES;
use tokio::io::AsyncBufReadExt;
/// `saturating_age`'s own arms are exact (`hive-sh4re::inbox`); what
/// is only checkable here is *which* stored timestamp each projection
@ -583,4 +611,70 @@ mod tests {
other => panic!("expected a Todo loose end, got {other:?}"),
}
}
/// Send `input` (then keep the write half open), return the one response line.
async fn exchange(input: Vec<u8>, deadlines: LineDeadlines) -> (Response, Result<()>) {
let (client, server) = UnixStream::pair().expect("socketpair");
let handler = tokio::spawn(serve_one(server, deadlines, |_| Response::Ok));
let (read, mut write) = client.into_split();
let _writer = tokio::spawn(async move {
let _ = write.write_all(&input).await;
std::future::pending::<()>().await;
});
let mut reply = String::new();
BufReader::new(read)
.read_line(&mut reply)
.await
.expect("reply");
let resp = serde_json::from_str(&reply).expect("response json");
(resp, handler.await.expect("join"))
}
#[tokio::test]
async fn a_valid_request_is_applied() {
let (resp, done) = exchange(b"{\"cmd\":\"compact\"}\n".to_vec(), REQUEST_DEADLINES).await;
assert!(matches!(resp, Response::Ok), "{resp:?}");
done.expect("served");
}
#[tokio::test]
async fn an_over_long_line_gets_an_error_not_an_unbounded_buffer() {
let (resp, done) =
exchange(vec![b'x'; REQUEST_LINE_MAX_BYTES + 1], REQUEST_DEADLINES).await;
assert!(
matches!(&resp, Response::Err { message } if message.starts_with("request too long")),
"{resp:?}"
);
done.expect("served");
}
#[tokio::test]
async fn a_sender_that_stalls_mid_line_times_out() {
let short = std::time::Duration::from_millis(50);
let deadlines = LineDeadlines {
idle: Some(short),
line: Some(short),
};
let (resp, done) = exchange(b"{\"cmd\"".to_vec(), deadlines).await;
assert!(
matches!(&resp, Response::Err { message } if message.starts_with("request timed out")),
"{resp:?}"
);
done.expect("served");
}
#[tokio::test]
async fn a_sender_that_never_writes_times_out() {
let short = std::time::Duration::from_millis(50);
let deadlines = LineDeadlines {
idle: Some(short),
line: Some(short),
};
let (resp, done) = exchange(Vec::new(), deadlines).await;
assert!(
matches!(&resp, Response::Err { message } if message.starts_with("request timed out")),
"{resp:?}"
);
done.expect("served");
}
}