fix(tines): harden IPC against malformed frames and lock poison

serve() now replies with Response::Error on a malformed JSON frame and
keeps the connection serving instead of dropping it on the parse error;
the per-connection loop only breaks on a clean disconnect
(UnexpectedEof). node.lock().unwrap() is replaced with poison-safe
into_inner() so one panicking handler can't take down the daemon.
Drop duplicate tines-core/tines-store/serde/serde_json from
dev-dependencies (only heapless is genuinely test-only).
This commit is contained in:
2026-10-18 17:15:14 -07:00
parent 20436badd0
commit 4f61bbde00
3 changed files with 63 additions and 16 deletions
-4
View File
@@ -18,8 +18,4 @@ serde = { version = "1", features = ["derive"] }
serde_json = "1" serde_json = "1"
[dev-dependencies] [dev-dependencies]
tines-core = { path = "../tines-core", version = "0.3.0" }
tines-store = { path = "../tines-store", version = "0.3.0" }
heapless = "0.8" heapless = "0.8"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
+23 -6
View File
@@ -199,20 +199,37 @@ where
for stream in listener.incoming() { for stream in listener.incoming() {
let mut stream = stream?; let mut stream = stream?;
// Serve this connection request-by-request until the peer disconnects // Serve this connection request-by-request until the peer cleanly
// or sends a malformed frame, then move on to the next connection. // disconnects, then move on to the next connection.
while let Ok(req) = read_request(&mut stream) { loop {
match read_request(&mut stream) {
Ok(req) => {
let now = now_ms(); let now = now_ms();
let resp = { let resp = {
let mut guard = node.lock().unwrap(); // Recover from a poisoned lock so one panicking
// handler can't take down the daemon.
let mut guard =
node.lock().unwrap_or_else(|poisoned| poisoned.into_inner());
handle(&mut guard, req, now) handle(&mut guard, req, now)
}; };
// A client that vanished mid-connection shouldn't take down the
// whole service; just drop this one socket.
if write_response(&mut stream, &resp).is_err() { if write_response(&mut stream, &resp).is_err() {
// A client that vanished mid-connection shouldn't
// take down the whole service; just drop this socket.
break; break;
} }
} }
// A clean disconnect (peer closed the socket) ends the loop.
Err(e) if e.kind() == std::io::ErrorKind::UnexpectedEof => break,
// A malformed frame isn't a disconnect: tell the client and
// keep serving this connection rather than dropping it. The
// frame can't be identified, so the error is generic.
Err(_) => {
if write_response(&mut stream, &error("malformed request")).is_err() {
break;
}
}
}
}
} }
Ok(()) Ok(())
} }
+34
View File
@@ -313,3 +313,37 @@ fn confidence_event_direct_probe() {
assert_eq!(evs[0].id, x); assert_eq!(evs[0].id, x);
assert_eq!(evs[0].peer, [7; 16]); assert_eq!(evs[0].peer, [7; 16]);
} }
/// A malformed JSON frame must yield an `Error` response and leave the
/// connection alive, so a following valid request is still served.
#[test]
fn malformed_frame_gets_error_and_connection_survives() {
let dir = unique_dir("malformed");
let socket = dir.join("ipc.sock");
let node = build_node([3; 16], &dir);
let serve_path = socket.clone();
let _handle = thread::spawn(move || serve(node, serve_path).unwrap());
let mut stream = connect(&socket);
// A length-prefixed frame whose body is not valid JSON.
let body = b"{ this is not valid json";
let len = (body.len() as u32).to_le_bytes();
stream.write_all(&len).unwrap();
stream.write_all(body).unwrap();
stream.flush().unwrap();
// The server must reply with an Error, not drop the socket.
match read_response(&mut stream) {
Response::Error { .. } => {}
other => panic!("expected Error for malformed frame, got {other:?}"),
}
// The connection is still alive: a valid request now succeeds.
rpc(&mut stream, &Request::Status);
match read_response(&mut stream) {
Response::Status { .. } => {}
other => panic!("expected Status after a malformed frame, got {other:?}"),
}
let _ = std::fs::remove_file(&socket);
}