From 77aa507e6ef21dff24593f9ead029f8679fef756 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 30 Aug 2026 06:14:04 -0700 Subject: [PATCH 001/105] test(network): require operational teardown assessment --- ...driver_bidi_session_teardown_assessment.rs | 159 ++++++++++++++++++ 1 file changed, 159 insertions(+) create mode 100644 crates/originweave-network/tests/webdriver_bidi_session_teardown_assessment.rs diff --git a/crates/originweave-network/tests/webdriver_bidi_session_teardown_assessment.rs b/crates/originweave-network/tests/webdriver_bidi_session_teardown_assessment.rs new file mode 100644 index 000000000..4cd44ac08 --- /dev/null +++ b/crates/originweave-network/tests/webdriver_bidi_session_teardown_assessment.rs @@ -0,0 +1,159 @@ +use std::{ + error::Error, + io::{self, Read, Write}, + net::{TcpListener, TcpStream}, + thread, + time::Duration, +}; + +use originweave_core::WebDriverBiDiWebSocketEndpoint; +use originweave_network::{ + WebDriverBiDiCommandCorrelation, WebDriverBiDiSessionEndCommand, + WebDriverBiDiSessionEndResult, WebDriverBiDiSessionTeardownAssessment, + WebDriverBiDiSessionTeardownDisposition, WebDriverBiDiSessionTeardownObservations, + WebDriverBiDiTcpConnectionPlan, WebDriverBiDiWebSocketClientKey, + WebDriverBiDiWebSocketHandshakePlan, WebDriverBiDiWebSocketMaskKey, + WebDriverBiDiWebSocketMessageAssembler, WebDriverBiDiWebSocketMessageAssembly, +}; + +const SESSION_ID: &str = "01234567-89ab-cdef-0123-456789abcdef"; +const RFC6455_SAMPLE_KEY: &str = "dGhlIHNhbXBsZSBub25jZQ=="; +const OPENING_RESPONSE: &[u8] = b"HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=\r\n\r\n"; +const END_SUCCESS_RESPONSE: &[u8] = br#"{"type":"success","id":7,"result":{}}"#; + +fn read_opening_request(stream: &mut TcpStream) -> io::Result<()> { + stream.set_read_timeout(Some(Duration::from_secs(2)))?; + let mut request = Vec::new(); + let mut buffer = [0_u8; 512]; + while !request.ends_with(b"\r\n\r\n") { + let count = stream.read(&mut buffer)?; + if count == 0 { + return Err(io::Error::new( + io::ErrorKind::UnexpectedEof, + "client opening request ended before the header terminator", + )); + } + request.extend_from_slice(&buffer[..count]); + } + Ok(()) +} + +fn read_masked_text_frame(stream: &mut TcpStream) -> io::Result> { + let mut header = [0_u8; 2]; + stream.read_exact(&mut header)?; + if header[0] != 0x81 || header[1] & 0x80 == 0 { + return Err(io::Error::new( + io::ErrorKind::InvalidData, + "expected one final masked client text frame", + )); + } + let length = usize::from(header[1] & 0x7f); + if length > 125 { + return Err(io::Error::new( + io::ErrorKind::InvalidData, + "session.end command unexpectedly required extended framing", + )); + } + let mut mask = [0_u8; 4]; + stream.read_exact(&mut mask)?; + let mut payload = vec![0_u8; length]; + stream.read_exact(&mut payload)?; + for (index, byte) in payload.iter_mut().enumerate() { + *byte ^= mask[index % mask.len()]; + } + Ok(payload) +} + +fn correlated_session_end_ack() -> Result> { + let listener = TcpListener::bind(("127.0.0.1", 0))?; + let local_addr = listener.local_addr()?; + let server = thread::spawn(move || -> io::Result<()> { + let (mut stream, _) = listener.accept()?; + read_opening_request(&mut stream)?; + stream.write_all(OPENING_RESPONSE)?; + let command = read_masked_text_frame(&mut stream)?; + if command != br#"{"id":7,"method":"session.end","params":{}}"# { + return Err(io::Error::new( + io::ErrorKind::InvalidData, + "unexpected session.end command", + )); + } + stream.write_all(&[0x81, END_SUCCESS_RESPONSE.len() as u8])?; + stream.write_all(END_SUCCESS_RESPONSE) + }); + + let endpoint = format!("ws://{local_addr}/session/{SESSION_ID}"); + let target = WebDriverBiDiWebSocketEndpoint::new(&endpoint)? + .correlate_session_id(SESSION_ID)? + .into_explicit_connect_target()?; + let connection = + WebDriverBiDiTcpConnectionPlan::new(target, Duration::from_secs(1), 1)?.connect()?; + let key = WebDriverBiDiWebSocketClientKey::new(RFC6455_SAMPLE_KEY)?; + let established = WebDriverBiDiWebSocketHandshakePlan::new(connection, key)? + .write_opening_request(Duration::from_millis(500))? + .read_opening_response(Duration::from_millis(500))?; + + let mut correlation = WebDriverBiDiCommandCorrelation::new(); + let established = WebDriverBiDiSessionEndCommand::new(7)?.send( + established, + &mut correlation, + WebDriverBiDiWebSocketMaskKey::new([1, 2, 3, 4]), + Duration::from_millis(500), + )?; + let (_established, frame) = established.read_frame(Duration::from_millis(500))?; + let mut assembler = WebDriverBiDiWebSocketMessageAssembler::new(); + let text = match assembler.push_frame(frame)? { + WebDriverBiDiWebSocketMessageAssembly::Text(text) => text, + other => { + return Err(io::Error::other(format!( + "session.end response produced unexpected assembly state: {other:?}" + )) + .into()); + } + }; + server + .join() + .map_err(|_| io::Error::other("session.end teardown test server panicked"))??; + Ok(WebDriverBiDiSessionEndResult::parse_and_correlate( + &text, + &mut correlation, + )?) +} + +#[test] +fn protocol_ack_does_not_claim_operational_teardown_without_all_observations() +-> Result<(), Box> { + let acknowledged = correlated_session_end_ack()?; + let assessment = WebDriverBiDiSessionTeardownAssessment::from_protocol_ack( + acknowledged, + WebDriverBiDiSessionTeardownObservations::new(true, false, true), + ); + + assert_eq!(assessment.command_id(), 7); + assert!(assessment.observations().transport_closed_observed()); + assert!(!assessment.observations().browser_process_exited_observed()); + assert!(assessment.observations().task_profile_removed_observed()); + assert!(!assessment.is_operationally_complete()); + assert_eq!( + assessment.disposition(), + WebDriverBiDiSessionTeardownDisposition::OperationalTeardownPending + ); + Ok(()) +} + +#[test] +fn correlated_ack_plus_all_operational_observations_is_complete() -> Result<(), Box> { + let acknowledged = correlated_session_end_ack()?; + let assessment = WebDriverBiDiSessionTeardownAssessment::from_protocol_ack( + acknowledged, + WebDriverBiDiSessionTeardownObservations::new(true, true, true), + ); + + assert!(assessment.is_operationally_complete()); + assert_eq!( + assessment.disposition(), + WebDriverBiDiSessionTeardownDisposition::OperationallyComplete + ); + assert_eq!(assessment.command_id(), 7); + Ok(()) +} From b5396ee3042ca24357a4b32bd80885f67a4646c0 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 30 Aug 2026 06:17:06 -0700 Subject: [PATCH 002/105] test(network): format teardown assessment regression --- .../webdriver_bidi_session_teardown_assessment.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/crates/originweave-network/tests/webdriver_bidi_session_teardown_assessment.rs b/crates/originweave-network/tests/webdriver_bidi_session_teardown_assessment.rs index 4cd44ac08..708132993 100644 --- a/crates/originweave-network/tests/webdriver_bidi_session_teardown_assessment.rs +++ b/crates/originweave-network/tests/webdriver_bidi_session_teardown_assessment.rs @@ -8,12 +8,12 @@ use std::{ use originweave_core::WebDriverBiDiWebSocketEndpoint; use originweave_network::{ - WebDriverBiDiCommandCorrelation, WebDriverBiDiSessionEndCommand, - WebDriverBiDiSessionEndResult, WebDriverBiDiSessionTeardownAssessment, - WebDriverBiDiSessionTeardownDisposition, WebDriverBiDiSessionTeardownObservations, - WebDriverBiDiTcpConnectionPlan, WebDriverBiDiWebSocketClientKey, - WebDriverBiDiWebSocketHandshakePlan, WebDriverBiDiWebSocketMaskKey, - WebDriverBiDiWebSocketMessageAssembler, WebDriverBiDiWebSocketMessageAssembly, + WebDriverBiDiCommandCorrelation, WebDriverBiDiSessionEndCommand, WebDriverBiDiSessionEndResult, + WebDriverBiDiSessionTeardownAssessment, WebDriverBiDiSessionTeardownDisposition, + WebDriverBiDiSessionTeardownObservations, WebDriverBiDiTcpConnectionPlan, + WebDriverBiDiWebSocketClientKey, WebDriverBiDiWebSocketHandshakePlan, + WebDriverBiDiWebSocketMaskKey, WebDriverBiDiWebSocketMessageAssembler, + WebDriverBiDiWebSocketMessageAssembly, }; const SESSION_ID: &str = "01234567-89ab-cdef-0123-456789abcdef"; From 7b0880a265e5c2a44ba8331cb71aa0ea1bab43af Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 30 Aug 2026 06:18:53 -0700 Subject: [PATCH 003/105] feat(network): separate protocol ack from teardown evidence --- .../src/webdriver_bidi_session_teardown.rs | 119 ++++++++++++++++++ 1 file changed, 119 insertions(+) create mode 100644 crates/originweave-network/src/webdriver_bidi_session_teardown.rs diff --git a/crates/originweave-network/src/webdriver_bidi_session_teardown.rs b/crates/originweave-network/src/webdriver_bidi_session_teardown.rs new file mode 100644 index 000000000..d214f4c0a --- /dev/null +++ b/crates/originweave-network/src/webdriver_bidi_session_teardown.rs @@ -0,0 +1,119 @@ +use crate::WebDriverBiDiSessionEndResult; + +/// Fail-closed operational disposition derived from explicit teardown observations. +/// +/// This value does not authenticate any observation or grant process, profile, browser, network, +/// policy, or Agent authority. `OperationallyComplete` means only that the caller supplied all +/// reviewed observation classes after a correlated `session.end` protocol acknowledgment. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum WebDriverBiDiSessionTeardownDisposition { + /// One or more required operational teardown observations remain absent. + OperationalTeardownPending, + /// Every required operational teardown observation was supplied. + OperationallyComplete, +} + +/// Explicit operational observations required after a correlated WebDriver BiDi `session.end` ack. +/// +/// These booleans are deliberately observation facts, not authority or evidence provenance. The +/// trusted browser/process/profile owner remains responsible for producing and authenticating the +/// underlying observations before constructing this value. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct WebDriverBiDiSessionTeardownObservations { + transport_closed_observed: bool, + browser_process_exited_observed: bool, + task_profile_removed_observed: bool, +} + +impl WebDriverBiDiSessionTeardownObservations { + /// Construct the three explicit operational observations required by this boundary. + #[must_use] + pub const fn new( + transport_closed_observed: bool, + browser_process_exited_observed: bool, + task_profile_removed_observed: bool, + ) -> Self { + Self { + transport_closed_observed, + browser_process_exited_observed, + task_profile_removed_observed, + } + } + + /// Return whether closure of the exact session transport was observed. + #[must_use] + pub const fn transport_closed_observed(&self) -> bool { + self.transport_closed_observed + } + + /// Return whether exit of the owned browser process was observed. + #[must_use] + pub const fn browser_process_exited_observed(&self) -> bool { + self.browser_process_exited_observed + } + + /// Return whether removal of the owned task profile was observed. + #[must_use] + pub const fn task_profile_removed_observed(&self) -> bool { + self.task_profile_removed_observed + } + + const fn operationally_complete(&self) -> bool { + self.transport_closed_observed + & self.browser_process_exited_observed + & self.task_profile_removed_observed + } +} + +/// One correlated `session.end` acknowledgment kept separate from operational teardown evidence. +/// +/// A protocol acknowledgment alone is never operational completion. Callers must separately +/// provide all reviewed transport/process/profile observations, and the observations themselves +/// remain non-authoritative until authenticated by their owning runtime boundary. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct WebDriverBiDiSessionTeardownAssessment { + protocol_ack: WebDriverBiDiSessionEndResult, + observations: WebDriverBiDiSessionTeardownObservations, +} + +impl WebDriverBiDiSessionTeardownAssessment { + /// Bind one correlated protocol acknowledgment to separately supplied operational observations. + #[must_use] + pub const fn from_protocol_ack( + protocol_ack: WebDriverBiDiSessionEndResult, + observations: WebDriverBiDiSessionTeardownObservations, + ) -> Self { + Self { + protocol_ack, + observations, + } + } + + /// Return the exact command id proven by the correlated protocol acknowledgment. + #[must_use] + pub const fn command_id(&self) -> u64 { + self.protocol_ack.command_id() + } + + /// Borrow the explicit operational observations bound to this assessment. + #[must_use] + pub const fn observations(&self) -> &WebDriverBiDiSessionTeardownObservations { + &self.observations + } + + /// Return whether all required operational teardown observations are present. + #[must_use] + pub const fn is_operationally_complete(&self) -> bool { + self.observations.operationally_complete() + } + + /// Return the fail-closed disposition for the currently supplied observations. + #[must_use] + pub const fn disposition(&self) -> WebDriverBiDiSessionTeardownDisposition { + if self.is_operationally_complete() { + WebDriverBiDiSessionTeardownDisposition::OperationallyComplete + } else { + WebDriverBiDiSessionTeardownDisposition::OperationalTeardownPending + } + } +} From 1dbf3a88afa7022b0a6c82a83dd5d0b47c4669a3 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 30 Aug 2026 06:19:17 -0700 Subject: [PATCH 004/105] feat(network): expose teardown assessment boundary --- crates/originweave-network/src/lib.rs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/crates/originweave-network/src/lib.rs b/crates/originweave-network/src/lib.rs index 35b1f7cc3..ff640fac3 100644 --- a/crates/originweave-network/src/lib.rs +++ b/crates/originweave-network/src/lib.rs @@ -9,8 +9,9 @@ //! unmasked server-frame reads, assembles bounded WebDriver BiDi text messages, //! classifies complete local-end JSON envelopes, tracks bounded command-response //! correlation, sends narrowly typed `session.status` and `session.end` commands, -//! and admits typed correlated status and end responses without exposing generic -//! JSON bodies or granting browser, TLS, policy, secret, or Agent authority. +//! admits typed correlated status and end responses, and keeps protocol teardown +//! acknowledgment separate from explicit operational teardown observations without +//! exposing generic JSON bodies or granting browser, TLS, policy, secret, or Agent authority. #![forbid(unsafe_code)] #![deny(missing_docs)] @@ -23,6 +24,7 @@ mod webdriver_bidi_session_end_command; mod webdriver_bidi_session_end_response; mod webdriver_bidi_session_status_command; mod webdriver_bidi_session_status_response; +mod webdriver_bidi_session_teardown; mod webdriver_bidi_websocket_frame; mod webdriver_bidi_websocket_handshake; mod webdriver_bidi_websocket_message; @@ -61,6 +63,10 @@ pub use webdriver_bidi_session_status_response::{ MAX_WEBDRIVER_BIDI_SESSION_STATUS_MESSAGE_SIZE, WebDriverBiDiSessionStatusResponseError, WebDriverBiDiSessionStatusResult, }; +pub use webdriver_bidi_session_teardown::{ + WebDriverBiDiSessionTeardownAssessment, WebDriverBiDiSessionTeardownDisposition, + WebDriverBiDiSessionTeardownObservations, +}; pub use webdriver_bidi_websocket_frame::{ MAX_WEBSOCKET_FRAME_PAYLOAD_SIZE, MAX_WEBSOCKET_FRAME_TIMEOUT, WebDriverBiDiWebSocketEstablished, WebDriverBiDiWebSocketFrame, From bfd2044208e0acfdcb8a4b6caa12945f2cf940a8 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 30 Aug 2026 06:31:32 -0700 Subject: [PATCH 005/105] test(network): require every teardown observation --- ...driver_bidi_session_teardown_assessment.rs | 52 +++++++++++++------ 1 file changed, 36 insertions(+), 16 deletions(-) diff --git a/crates/originweave-network/tests/webdriver_bidi_session_teardown_assessment.rs b/crates/originweave-network/tests/webdriver_bidi_session_teardown_assessment.rs index 708132993..60cc40f61 100644 --- a/crates/originweave-network/tests/webdriver_bidi_session_teardown_assessment.rs +++ b/crates/originweave-network/tests/webdriver_bidi_session_teardown_assessment.rs @@ -121,23 +121,43 @@ fn correlated_session_end_ack() -> Result Result<(), Box> { - let acknowledged = correlated_session_end_ack()?; - let assessment = WebDriverBiDiSessionTeardownAssessment::from_protocol_ack( - acknowledged, - WebDriverBiDiSessionTeardownObservations::new(true, false, true), - ); +fn every_missing_operational_observation_keeps_teardown_pending() -> Result<(), Box> { + let incomplete_observations = [ + (false, true, true), + (true, false, true), + (true, true, false), + ]; - assert_eq!(assessment.command_id(), 7); - assert!(assessment.observations().transport_closed_observed()); - assert!(!assessment.observations().browser_process_exited_observed()); - assert!(assessment.observations().task_profile_removed_observed()); - assert!(!assessment.is_operationally_complete()); - assert_eq!( - assessment.disposition(), - WebDriverBiDiSessionTeardownDisposition::OperationalTeardownPending - ); + for (transport_closed, browser_exited, profile_removed) in incomplete_observations { + let acknowledged = correlated_session_end_ack()?; + let assessment = WebDriverBiDiSessionTeardownAssessment::from_protocol_ack( + acknowledged, + WebDriverBiDiSessionTeardownObservations::new( + transport_closed, + browser_exited, + profile_removed, + ), + ); + + assert_eq!(assessment.command_id(), 7); + assert_eq!( + assessment.observations().transport_closed_observed(), + transport_closed + ); + assert_eq!( + assessment.observations().browser_process_exited_observed(), + browser_exited + ); + assert_eq!( + assessment.observations().task_profile_removed_observed(), + profile_removed + ); + assert!(!assessment.is_operationally_complete()); + assert_eq!( + assessment.disposition(), + WebDriverBiDiSessionTeardownDisposition::OperationalTeardownPending + ); + } Ok(()) } From 0fff9816a1534224cd30a238f2f36bc55818fdd9 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 30 Aug 2026 06:40:45 -0700 Subject: [PATCH 006/105] test(network): require typed BiDi transport closure evidence --- ...webdriver_bidi_transport_close_evidence.rs | 129 ++++++++++++++++++ 1 file changed, 129 insertions(+) create mode 100644 crates/originweave-network/tests/webdriver_bidi_transport_close_evidence.rs diff --git a/crates/originweave-network/tests/webdriver_bidi_transport_close_evidence.rs b/crates/originweave-network/tests/webdriver_bidi_transport_close_evidence.rs new file mode 100644 index 000000000..e12217d33 --- /dev/null +++ b/crates/originweave-network/tests/webdriver_bidi_transport_close_evidence.rs @@ -0,0 +1,129 @@ +use std::{ + error::Error, + io::{self, Read, Write}, + net::{TcpListener, TcpStream}, + thread, + time::Duration, +}; + +use originweave_core::WebDriverBiDiWebSocketEndpoint; +use originweave_network::{ + WebDriverBiDiTcpConnectionPlan, WebDriverBiDiWebSocketClientKey, + WebDriverBiDiWebSocketHandshakePlan, WebDriverBiDiWebSocketTransportClosureError, + WebDriverBiDiWebSocketTransportClosureKind, WebDriverBiDiWebSocketTransportClosureObservation, +}; + +const SESSION_ID: &str = "01234567-89ab-cdef-0123-456789abcdef"; +const RFC6455_SAMPLE_KEY: &str = "dGhlIHNhbXBsZSBub25jZQ=="; +const OPENING_RESPONSE: &[u8] = b"HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=\r\n\r\n"; + +fn read_opening_request(stream: &mut TcpStream) -> io::Result<()> { + stream.set_read_timeout(Some(Duration::from_secs(2)))?; + let mut request = Vec::new(); + let mut buffer = [0_u8; 512]; + while !request.ends_with(b"\r\n\r\n") { + let count = stream.read(&mut buffer)?; + if count == 0 { + return Err(io::Error::new( + io::ErrorKind::UnexpectedEof, + "client opening request ended before the header terminator", + )); + } + request.extend_from_slice(&buffer[..count]); + } + Ok(()) +} + +fn established_with_server_frame( + frame: Option<&'static [u8]>, +) -> Result< + ( + originweave_network::WebDriverBiDiWebSocketEstablished, + thread::JoinHandle>, + ), + Box, +> { + let listener = TcpListener::bind(("127.0.0.1", 0))?; + let local_addr = listener.local_addr()?; + let server = thread::spawn(move || -> io::Result<()> { + let (mut stream, _) = listener.accept()?; + read_opening_request(&mut stream)?; + stream.write_all(OPENING_RESPONSE)?; + if let Some(frame) = frame { + stream.write_all(frame)?; + } + Ok(()) + }); + + let endpoint = format!("ws://{local_addr}/session/{SESSION_ID}"); + let target = WebDriverBiDiWebSocketEndpoint::new(&endpoint)? + .correlate_session_id(SESSION_ID)? + .into_explicit_connect_target()?; + let connection = + WebDriverBiDiTcpConnectionPlan::new(target, Duration::from_secs(1), 1)?.connect()?; + let key = WebDriverBiDiWebSocketClientKey::new(RFC6455_SAMPLE_KEY)?; + let established = WebDriverBiDiWebSocketHandshakePlan::new(connection, key)? + .write_opening_request(Duration::from_millis(500))? + .read_opening_response(Duration::from_millis(500))?; + Ok((established, server)) +} + +#[test] +fn validated_peer_close_frame_yields_nonforgeable_transport_observation() -> Result<(), Box> { + let (established, server) = established_with_server_frame(Some(&[0x88, 0x02, 0x03, 0xe8]))?; + + let observation = WebDriverBiDiWebSocketTransportClosureObservation::observe( + established, + Duration::from_millis(500), + )?; + + server + .join() + .map_err(|_| io::Error::other("transport-close test server panicked"))??; + assert_eq!( + observation.kind(), + WebDriverBiDiWebSocketTransportClosureKind::PeerCloseFrame + ); + assert_eq!(observation.peer_close_status_code(), Some(1000)); + Ok(()) +} + +#[test] +fn clean_peer_eof_yields_transport_observation_without_inventing_close_status() -> Result<(), Box> { + let (established, server) = established_with_server_frame(None)?; + + let observation = WebDriverBiDiWebSocketTransportClosureObservation::observe( + established, + Duration::from_millis(500), + )?; + + server + .join() + .map_err(|_| io::Error::other("transport-eof test server panicked"))??; + assert_eq!( + observation.kind(), + WebDriverBiDiWebSocketTransportClosureKind::PeerEof + ); + assert_eq!(observation.peer_close_status_code(), None); + Ok(()) +} + +#[test] +fn application_frame_after_teardown_does_not_become_closure_evidence() -> Result<(), Box> { + let (established, server) = established_with_server_frame(Some(&[0x81, 0x02, b'o', b'k']))?; + + let error = WebDriverBiDiWebSocketTransportClosureObservation::observe( + established, + Duration::from_millis(500), + ) + .expect_err("an application frame must not become transport-closure evidence"); + + server + .join() + .map_err(|_| io::Error::other("unexpected-frame test server panicked"))??; + assert!(matches!( + error, + WebDriverBiDiWebSocketTransportClosureError::UnexpectedFrame { opcode: 0x1 } + )); + Ok(()) +} From 4bc7c3ce22f24a9935f541ff89ae229cdc710541 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 30 Aug 2026 06:43:47 -0700 Subject: [PATCH 007/105] style(network): apply canonical transport-close test formatting --- .../tests/webdriver_bidi_transport_close_evidence.rs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/crates/originweave-network/tests/webdriver_bidi_transport_close_evidence.rs b/crates/originweave-network/tests/webdriver_bidi_transport_close_evidence.rs index e12217d33..55e402aa6 100644 --- a/crates/originweave-network/tests/webdriver_bidi_transport_close_evidence.rs +++ b/crates/originweave-network/tests/webdriver_bidi_transport_close_evidence.rs @@ -69,7 +69,8 @@ fn established_with_server_frame( } #[test] -fn validated_peer_close_frame_yields_nonforgeable_transport_observation() -> Result<(), Box> { +fn validated_peer_close_frame_yields_nonforgeable_transport_observation() +-> Result<(), Box> { let (established, server) = established_with_server_frame(Some(&[0x88, 0x02, 0x03, 0xe8]))?; let observation = WebDriverBiDiWebSocketTransportClosureObservation::observe( @@ -89,7 +90,8 @@ fn validated_peer_close_frame_yields_nonforgeable_transport_observation() -> Res } #[test] -fn clean_peer_eof_yields_transport_observation_without_inventing_close_status() -> Result<(), Box> { +fn clean_peer_eof_yields_transport_observation_without_inventing_close_status() +-> Result<(), Box> { let (established, server) = established_with_server_frame(None)?; let observation = WebDriverBiDiWebSocketTransportClosureObservation::observe( @@ -109,7 +111,8 @@ fn clean_peer_eof_yields_transport_observation_without_inventing_close_status() } #[test] -fn application_frame_after_teardown_does_not_become_closure_evidence() -> Result<(), Box> { +fn application_frame_after_teardown_does_not_become_closure_evidence() -> Result<(), Box> +{ let (established, server) = established_with_server_frame(Some(&[0x81, 0x02, b'o', b'k']))?; let error = WebDriverBiDiWebSocketTransportClosureObservation::observe( From 916f6d4907a407e6ddcba244817d3cb64e5c7180 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 30 Aug 2026 06:45:41 -0700 Subject: [PATCH 008/105] feat(network): observe typed BiDi transport closure --- ...driver_bidi_websocket_transport_closure.rs | 112 ++++++++++++++++++ 1 file changed, 112 insertions(+) create mode 100644 crates/originweave-network/src/webdriver_bidi_websocket_transport_closure.rs diff --git a/crates/originweave-network/src/webdriver_bidi_websocket_transport_closure.rs b/crates/originweave-network/src/webdriver_bidi_websocket_transport_closure.rs new file mode 100644 index 000000000..7dfba3cc3 --- /dev/null +++ b/crates/originweave-network/src/webdriver_bidi_websocket_transport_closure.rs @@ -0,0 +1,112 @@ +use std::{error::Error, fmt, time::Duration}; + +use crate::{WebDriverBiDiWebSocketEstablished, WebDriverBiDiWebSocketFrameError}; + +/// Bounded transport-closure condition observed on one consumed WebDriver BiDi WebSocket. +/// +/// This classification proves only what the already session-correlated transport itself exposed. +/// A peer Close frame does not prove browser-process exit or profile cleanup, while peer EOF does +/// not imply that the RFC 6455 closing handshake completed. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum WebDriverBiDiWebSocketTransportClosureKind { + /// The peer sent one RFC 6455 Close frame that passed the existing strict frame validator. + PeerCloseFrame, + /// The peer ended the TCP byte stream cleanly before any new WebSocket frame byte was read. + PeerEof, +} + +/// Credential-free observation that one established WebDriver BiDi transport ceased carrying data. +/// +/// Construction consumes the established WebSocket, so this value cannot be used to regain the +/// underlying connection. It records only a validated peer Close status when one was actually +/// present, or clean TCP EOF before a new frame began. It grants no browser, process, profile, +/// policy, secret, retry, reconnect, or Agent authority and does not perform a reciprocal Close +/// handshake. +#[derive(Debug, Eq, PartialEq)] +pub struct WebDriverBiDiWebSocketTransportClosureObservation { + kind: WebDriverBiDiWebSocketTransportClosureKind, + peer_close_status_code: Option, +} + +impl WebDriverBiDiWebSocketTransportClosureObservation { + /// Consume one established connection and observe one bounded transport-closure condition. + /// + /// A validated peer Close frame and zero-byte clean EOF are the only success cases. Any data + /// frame, partial-frame EOF, timeout, malformed Close, I/O failure, or integrity failure remains + /// a typed error and is never normalized into successful teardown evidence. + pub fn observe( + established: WebDriverBiDiWebSocketEstablished, + frame_timeout: Duration, + ) -> Result { + match established.read_frame(frame_timeout) { + Ok((established, frame)) => { + if frame.opcode() != 0x8 { + return Err(WebDriverBiDiWebSocketTransportClosureError::UnexpectedFrame { + opcode: frame.opcode(), + }); + } + let peer_close_status_code = frame + .payload() + .get(..2) + .map(|bytes| u16::from_be_bytes([bytes[0], bytes[1]])); + drop(established); + Ok(Self { + kind: WebDriverBiDiWebSocketTransportClosureKind::PeerCloseFrame, + peer_close_status_code, + }) + } + Err(WebDriverBiDiWebSocketFrameError::FrameEnded { bytes_read: 0 }) => Ok(Self { + kind: WebDriverBiDiWebSocketTransportClosureKind::PeerEof, + peer_close_status_code: None, + }), + Err(source) => Err(WebDriverBiDiWebSocketTransportClosureError::Frame { source }), + } + } + + /// Return the exact transport-closure condition that produced this observation. + #[must_use] + pub const fn kind(&self) -> WebDriverBiDiWebSocketTransportClosureKind { + self.kind + } + + /// Return the validated peer Close status when a Close frame actually carried one. + #[must_use] + pub const fn peer_close_status_code(&self) -> Option { + self.peer_close_status_code + } +} + +/// Fail-closed errors while converting one established BiDi transport into closure evidence. +#[derive(Debug)] +pub enum WebDriverBiDiWebSocketTransportClosureError { + /// The peer sent a valid WebSocket frame that was not a Close frame. + UnexpectedFrame { + /// Exact validated RFC 6455 opcode observed instead of a Close frame. + opcode: u8, + }, + /// The existing bounded WebSocket frame reader failed before closure was proven. + Frame { + /// Original typed frame failure retained as the causal source. + source: WebDriverBiDiWebSocketFrameError, + }, +} + +impl fmt::Display for WebDriverBiDiWebSocketTransportClosureError { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::UnexpectedFrame { .. } => formatter + .write_str("WebDriver BiDi peer sent application traffic instead of closing"), + Self::Frame { .. } => formatter + .write_str("WebDriver BiDi transport closure could not be observed safely"), + } + } +} + +impl Error for WebDriverBiDiWebSocketTransportClosureError { + fn source(&self) -> Option<&(dyn Error + 'static)> { + match self { + Self::UnexpectedFrame { .. } => None, + Self::Frame { source } => Some(source), + } + } +} From eed38a3467ad74a02be43427cefb11e8c56e1765 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 30 Aug 2026 06:46:23 -0700 Subject: [PATCH 009/105] feat(network): export bounded BiDi transport closure evidence --- crates/originweave-network/src/lib.rs | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/crates/originweave-network/src/lib.rs b/crates/originweave-network/src/lib.rs index ff640fac3..7400cc4d0 100644 --- a/crates/originweave-network/src/lib.rs +++ b/crates/originweave-network/src/lib.rs @@ -9,9 +9,10 @@ //! unmasked server-frame reads, assembles bounded WebDriver BiDi text messages, //! classifies complete local-end JSON envelopes, tracks bounded command-response //! correlation, sends narrowly typed `session.status` and `session.end` commands, -//! admits typed correlated status and end responses, and keeps protocol teardown -//! acknowledgment separate from explicit operational teardown observations without -//! exposing generic JSON bodies or granting browser, TLS, policy, secret, or Agent authority. +//! admits typed correlated status and end responses, observes bounded peer Close +//! or clean-EOF transport cessation, and keeps protocol/transport evidence separate +//! from explicit operational teardown observations without exposing generic JSON +//! bodies or granting browser, TLS, policy, secret, process, profile, or Agent authority. #![forbid(unsafe_code)] #![deny(missing_docs)] @@ -29,6 +30,7 @@ mod webdriver_bidi_websocket_frame; mod webdriver_bidi_websocket_handshake; mod webdriver_bidi_websocket_message; mod webdriver_bidi_websocket_opening_recovery; +mod webdriver_bidi_websocket_transport_closure; #[cfg(test)] mod webdriver_bidi_json_envelope_public_boundary_tests; @@ -86,3 +88,7 @@ pub use webdriver_bidi_websocket_message::{ WebDriverBiDiWebSocketTextMessage, }; pub use webdriver_bidi_websocket_opening_recovery::WebDriverBiDiWebSocketOpeningWriteRecoveryDisposition; +pub use webdriver_bidi_websocket_transport_closure::{ + WebDriverBiDiWebSocketTransportClosureError, WebDriverBiDiWebSocketTransportClosureKind, + WebDriverBiDiWebSocketTransportClosureObservation, +}; From 69cfe4c208db7b25ea843147510aae5b87d402b6 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 30 Aug 2026 06:47:59 -0700 Subject: [PATCH 010/105] style(network): apply canonical transport-closure formatting --- .../webdriver_bidi_websocket_transport_closure.rs | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/crates/originweave-network/src/webdriver_bidi_websocket_transport_closure.rs b/crates/originweave-network/src/webdriver_bidi_websocket_transport_closure.rs index 7dfba3cc3..3100d773a 100644 --- a/crates/originweave-network/src/webdriver_bidi_websocket_transport_closure.rs +++ b/crates/originweave-network/src/webdriver_bidi_websocket_transport_closure.rs @@ -41,9 +41,11 @@ impl WebDriverBiDiWebSocketTransportClosureObservation { match established.read_frame(frame_timeout) { Ok((established, frame)) => { if frame.opcode() != 0x8 { - return Err(WebDriverBiDiWebSocketTransportClosureError::UnexpectedFrame { - opcode: frame.opcode(), - }); + return Err( + WebDriverBiDiWebSocketTransportClosureError::UnexpectedFrame { + opcode: frame.opcode(), + }, + ); } let peer_close_status_code = frame .payload() @@ -96,8 +98,9 @@ impl fmt::Display for WebDriverBiDiWebSocketTransportClosureError { match self { Self::UnexpectedFrame { .. } => formatter .write_str("WebDriver BiDi peer sent application traffic instead of closing"), - Self::Frame { .. } => formatter - .write_str("WebDriver BiDi transport closure could not be observed safely"), + Self::Frame { .. } => { + formatter.write_str("WebDriver BiDi transport closure could not be observed safely") + } } } } From 6612af70809c596301b446ab8ad6df42f40322cf Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 30 Aug 2026 08:34:18 -0700 Subject: [PATCH 011/105] test(network): satisfy strict transport-closure contracts --- ...webdriver_bidi_transport_close_evidence.rs | 23 +++++++++++-------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/crates/originweave-network/tests/webdriver_bidi_transport_close_evidence.rs b/crates/originweave-network/tests/webdriver_bidi_transport_close_evidence.rs index 55e402aa6..306ed780e 100644 --- a/crates/originweave-network/tests/webdriver_bidi_transport_close_evidence.rs +++ b/crates/originweave-network/tests/webdriver_bidi_transport_close_evidence.rs @@ -17,6 +17,11 @@ const SESSION_ID: &str = "01234567-89ab-cdef-0123-456789abcdef"; const RFC6455_SAMPLE_KEY: &str = "dGhlIHNhbXBsZSBub25jZQ=="; const OPENING_RESPONSE: &[u8] = b"HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=\r\n\r\n"; +type EstablishedWithServer = ( + originweave_network::WebDriverBiDiWebSocketEstablished, + thread::JoinHandle>, +); + fn read_opening_request(stream: &mut TcpStream) -> io::Result<()> { stream.set_read_timeout(Some(Duration::from_secs(2)))?; let mut request = Vec::new(); @@ -36,13 +41,7 @@ fn read_opening_request(stream: &mut TcpStream) -> io::Result<()> { fn established_with_server_frame( frame: Option<&'static [u8]>, -) -> Result< - ( - originweave_network::WebDriverBiDiWebSocketEstablished, - thread::JoinHandle>, - ), - Box, -> { +) -> Result> { let listener = TcpListener::bind(("127.0.0.1", 0))?; let local_addr = listener.local_addr()?; let server = thread::spawn(move || -> io::Result<()> { @@ -115,11 +114,15 @@ fn application_frame_after_teardown_does_not_become_closure_evidence() -> Result { let (established, server) = established_with_server_frame(Some(&[0x81, 0x02, b'o', b'k']))?; - let error = WebDriverBiDiWebSocketTransportClosureObservation::observe( + let Err(error) = WebDriverBiDiWebSocketTransportClosureObservation::observe( established, Duration::from_millis(500), - ) - .expect_err("an application frame must not become transport-closure evidence"); + ) else { + return Err(io::Error::other( + "application frame unexpectedly became transport-closure evidence", + ) + .into()); + }; server .join() From fa68bb9b10bfd888b620131c396494da022fe472 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 30 Aug 2026 08:35:39 -0700 Subject: [PATCH 012/105] test(network): cover fail-closed transport closure errors --- ...webdriver_bidi_transport_close_evidence.rs | 36 ++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-) diff --git a/crates/originweave-network/tests/webdriver_bidi_transport_close_evidence.rs b/crates/originweave-network/tests/webdriver_bidi_transport_close_evidence.rs index 306ed780e..76a8aa35f 100644 --- a/crates/originweave-network/tests/webdriver_bidi_transport_close_evidence.rs +++ b/crates/originweave-network/tests/webdriver_bidi_transport_close_evidence.rs @@ -128,8 +128,42 @@ fn application_frame_after_teardown_does_not_become_closure_evidence() -> Result .join() .map_err(|_| io::Error::other("unexpected-frame test server panicked"))??; assert!(matches!( - error, + &error, WebDriverBiDiWebSocketTransportClosureError::UnexpectedFrame { opcode: 0x1 } )); + assert_eq!( + error.to_string(), + "WebDriver BiDi peer sent application traffic instead of closing" + ); + assert!(error.source().is_none()); + Ok(()) +} + +#[test] +fn malformed_close_frame_remains_a_typed_frame_failure() -> Result<(), Box> { + let (established, server) = established_with_server_frame(Some(&[0x88, 0x01, 0x00]))?; + + let Err(error) = WebDriverBiDiWebSocketTransportClosureObservation::observe( + established, + Duration::from_millis(500), + ) else { + return Err(io::Error::other( + "malformed close frame unexpectedly became transport-closure evidence", + ) + .into()); + }; + + server + .join() + .map_err(|_| io::Error::other("malformed-close test server panicked"))??; + assert!(matches!( + &error, + WebDriverBiDiWebSocketTransportClosureError::Frame { .. } + )); + assert_eq!( + error.to_string(), + "WebDriver BiDi transport closure could not be observed safely" + ); + assert!(error.source().is_some()); Ok(()) } From f956341f3c036b4b85c845b7632b36f7008046e2 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 30 Aug 2026 09:02:03 -0700 Subject: [PATCH 013/105] test(network): require pong-tolerant BiDi transport close --- ...webdriver_bidi_transport_close_evidence.rs | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/crates/originweave-network/tests/webdriver_bidi_transport_close_evidence.rs b/crates/originweave-network/tests/webdriver_bidi_transport_close_evidence.rs index 76a8aa35f..cd7b14790 100644 --- a/crates/originweave-network/tests/webdriver_bidi_transport_close_evidence.rs +++ b/crates/originweave-network/tests/webdriver_bidi_transport_close_evidence.rs @@ -88,6 +88,28 @@ fn validated_peer_close_frame_yields_nonforgeable_transport_observation() Ok(()) } +#[test] +fn one_unsolicited_pong_before_close_does_not_block_closure_observation() +-> Result<(), Box> { + let (established, server) = + established_with_server_frame(Some(&[0x8a, 0x00, 0x88, 0x02, 0x03, 0xe8]))?; + + let observation = WebDriverBiDiWebSocketTransportClosureObservation::observe( + established, + Duration::from_millis(500), + )?; + + server + .join() + .map_err(|_| io::Error::other("pong-before-close test server panicked"))??; + assert_eq!( + observation.kind(), + WebDriverBiDiWebSocketTransportClosureKind::PeerCloseFrame + ); + assert_eq!(observation.peer_close_status_code(), Some(1000)); + Ok(()) +} + #[test] fn clean_peer_eof_yields_transport_observation_without_inventing_close_status() -> Result<(), Box> { From 80f50008041ae81f770752e4381861aba6089ee9 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 30 Aug 2026 09:04:06 -0700 Subject: [PATCH 014/105] fix(network): tolerate bounded pong before BiDi transport close --- ...driver_bidi_websocket_transport_closure.rs | 26 ++++++++++++++----- 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/crates/originweave-network/src/webdriver_bidi_websocket_transport_closure.rs b/crates/originweave-network/src/webdriver_bidi_websocket_transport_closure.rs index 3100d773a..bea19f905 100644 --- a/crates/originweave-network/src/webdriver_bidi_websocket_transport_closure.rs +++ b/crates/originweave-network/src/webdriver_bidi_websocket_transport_closure.rs @@ -31,14 +31,28 @@ pub struct WebDriverBiDiWebSocketTransportClosureObservation { impl WebDriverBiDiWebSocketTransportClosureObservation { /// Consume one established connection and observe one bounded transport-closure condition. /// - /// A validated peer Close frame and zero-byte clean EOF are the only success cases. Any data - /// frame, partial-frame EOF, timeout, malformed Close, I/O failure, or integrity failure remains - /// a typed error and is never normalized into successful teardown evidence. + /// A validated peer Close frame and zero-byte clean EOF are the only success cases. One + /// unsolicited Pong may be ignored before that closure signal because Pong requires no client + /// response; a second Pong, Ping, data frame, partial-frame EOF, timeout, malformed Close, I/O + /// failure, or integrity failure remains a typed error. This fixed two-read envelope prevents + /// peer control traffic from extending teardown observation indefinitely and does not invent + /// masking entropy or outbound authority to answer Ping frames. pub fn observe( established: WebDriverBiDiWebSocketEstablished, frame_timeout: Duration, + ) -> Result { + Self::observe_frame(established, frame_timeout, true) + } + + fn observe_frame( + established: WebDriverBiDiWebSocketEstablished, + frame_timeout: Duration, + allow_pong: bool, ) -> Result { match established.read_frame(frame_timeout) { + Ok((established, frame)) if frame.opcode() == 0xa && allow_pong => { + Self::observe_frame(established, frame_timeout, false) + } Ok((established, frame)) => { if frame.opcode() != 0x8 { return Err( @@ -81,9 +95,9 @@ impl WebDriverBiDiWebSocketTransportClosureObservation { /// Fail-closed errors while converting one established BiDi transport into closure evidence. #[derive(Debug)] pub enum WebDriverBiDiWebSocketTransportClosureError { - /// The peer sent a valid WebSocket frame that was not a Close frame. + /// The peer sent a valid WebSocket frame that was not an admissible closure signal. UnexpectedFrame { - /// Exact validated RFC 6455 opcode observed instead of a Close frame. + /// Exact validated RFC 6455 opcode observed instead of an admissible closure signal. opcode: u8, }, /// The existing bounded WebSocket frame reader failed before closure was proven. @@ -97,7 +111,7 @@ impl fmt::Display for WebDriverBiDiWebSocketTransportClosureError { fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Self::UnexpectedFrame { .. } => formatter - .write_str("WebDriver BiDi peer sent application traffic instead of closing"), + .write_str("WebDriver BiDi peer sent non-closure traffic instead of closing"), Self::Frame { .. } => { formatter.write_str("WebDriver BiDi transport closure could not be observed safely") } From 01cfce2c2c1c89c31cac72d6a53979e78b6c24d9 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 30 Aug 2026 09:04:37 -0700 Subject: [PATCH 015/105] test(network): bound pong handling during BiDi transport close --- ...webdriver_bidi_transport_close_evidence.rs | 32 ++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/crates/originweave-network/tests/webdriver_bidi_transport_close_evidence.rs b/crates/originweave-network/tests/webdriver_bidi_transport_close_evidence.rs index cd7b14790..895e54b10 100644 --- a/crates/originweave-network/tests/webdriver_bidi_transport_close_evidence.rs +++ b/crates/originweave-network/tests/webdriver_bidi_transport_close_evidence.rs @@ -110,6 +110,36 @@ fn one_unsolicited_pong_before_close_does_not_block_closure_observation() Ok(()) } +#[test] +fn repeated_pong_frames_remain_fail_closed_under_fixed_read_budget() -> Result<(), Box> { + let (established, server) = + established_with_server_frame(Some(&[0x8a, 0x00, 0x8a, 0x00]))?; + + let Err(error) = WebDriverBiDiWebSocketTransportClosureObservation::observe( + established, + Duration::from_millis(500), + ) else { + return Err(io::Error::other( + "repeated Pong frames unexpectedly became transport-closure evidence", + ) + .into()); + }; + + server + .join() + .map_err(|_| io::Error::other("repeated-pong test server panicked"))??; + assert!(matches!( + &error, + WebDriverBiDiWebSocketTransportClosureError::UnexpectedFrame { opcode: 0xa } + )); + assert_eq!( + error.to_string(), + "WebDriver BiDi peer sent non-closure traffic instead of closing" + ); + assert!(error.source().is_none()); + Ok(()) +} + #[test] fn clean_peer_eof_yields_transport_observation_without_inventing_close_status() -> Result<(), Box> { @@ -155,7 +185,7 @@ fn application_frame_after_teardown_does_not_become_closure_evidence() -> Result )); assert_eq!( error.to_string(), - "WebDriver BiDi peer sent application traffic instead of closing" + "WebDriver BiDi peer sent non-closure traffic instead of closing" ); assert!(error.source().is_none()); Ok(()) From a45812dcbb742ac134764b3e4c3de3429b434575 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 30 Aug 2026 09:11:38 -0700 Subject: [PATCH 016/105] style(network): apply canonical rustfmt to transport close tests --- .../tests/webdriver_bidi_transport_close_evidence.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/crates/originweave-network/tests/webdriver_bidi_transport_close_evidence.rs b/crates/originweave-network/tests/webdriver_bidi_transport_close_evidence.rs index 895e54b10..073ecb790 100644 --- a/crates/originweave-network/tests/webdriver_bidi_transport_close_evidence.rs +++ b/crates/originweave-network/tests/webdriver_bidi_transport_close_evidence.rs @@ -112,8 +112,7 @@ fn one_unsolicited_pong_before_close_does_not_block_closure_observation() #[test] fn repeated_pong_frames_remain_fail_closed_under_fixed_read_budget() -> Result<(), Box> { - let (established, server) = - established_with_server_frame(Some(&[0x8a, 0x00, 0x8a, 0x00]))?; + let (established, server) = established_with_server_frame(Some(&[0x8a, 0x00, 0x8a, 0x00]))?; let Err(error) = WebDriverBiDiWebSocketTransportClosureObservation::observe( established, From 5ff8ae171511e6c089aa240e945289b3f61b9505 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 30 Aug 2026 09:17:49 -0700 Subject: [PATCH 017/105] test(network): require typed transport evidence for BiDi teardown --- ...driver_bidi_session_teardown_assessment.rs | 73 +++++++++++++++---- 1 file changed, 57 insertions(+), 16 deletions(-) diff --git a/crates/originweave-network/tests/webdriver_bidi_session_teardown_assessment.rs b/crates/originweave-network/tests/webdriver_bidi_session_teardown_assessment.rs index 60cc40f61..14e51e3dc 100644 --- a/crates/originweave-network/tests/webdriver_bidi_session_teardown_assessment.rs +++ b/crates/originweave-network/tests/webdriver_bidi_session_teardown_assessment.rs @@ -11,15 +11,17 @@ use originweave_network::{ WebDriverBiDiCommandCorrelation, WebDriverBiDiSessionEndCommand, WebDriverBiDiSessionEndResult, WebDriverBiDiSessionTeardownAssessment, WebDriverBiDiSessionTeardownDisposition, WebDriverBiDiSessionTeardownObservations, WebDriverBiDiTcpConnectionPlan, - WebDriverBiDiWebSocketClientKey, WebDriverBiDiWebSocketHandshakePlan, - WebDriverBiDiWebSocketMaskKey, WebDriverBiDiWebSocketMessageAssembler, - WebDriverBiDiWebSocketMessageAssembly, + WebDriverBiDiWebSocketClientKey, WebDriverBiDiWebSocketEstablished, + WebDriverBiDiWebSocketHandshakePlan, WebDriverBiDiWebSocketMaskKey, + WebDriverBiDiWebSocketMessageAssembler, WebDriverBiDiWebSocketMessageAssembly, + WebDriverBiDiWebSocketTransportClosureKind, WebDriverBiDiWebSocketTransportClosureObservation, }; const SESSION_ID: &str = "01234567-89ab-cdef-0123-456789abcdef"; const RFC6455_SAMPLE_KEY: &str = "dGhlIHNhbXBsZSBub25jZQ=="; const OPENING_RESPONSE: &[u8] = b"HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=\r\n\r\n"; const END_SUCCESS_RESPONSE: &[u8] = br#"{"type":"success","id":7,"result":{}}"#; +const NORMAL_CLOSE_FRAME: &[u8] = &[0x88, 0x02, 0x03, 0xe8]; fn read_opening_request(stream: &mut TcpStream) -> io::Result<()> { stream.set_read_timeout(Some(Duration::from_secs(2)))?; @@ -64,7 +66,8 @@ fn read_masked_text_frame(stream: &mut TcpStream) -> io::Result> { Ok(payload) } -fn correlated_session_end_ack() -> Result> { +fn correlated_session_end_ack_and_transport() +-> Result<(WebDriverBiDiSessionEndResult, WebDriverBiDiWebSocketEstablished), Box> { let listener = TcpListener::bind(("127.0.0.1", 0))?; let local_addr = listener.local_addr()?; let server = thread::spawn(move || -> io::Result<()> { @@ -79,7 +82,8 @@ fn correlated_session_end_ack() -> Result Result text, @@ -111,17 +115,32 @@ fn correlated_session_end_ack() -> Result Result> { + let observation = WebDriverBiDiWebSocketTransportClosureObservation::observe( + established, + Duration::from_millis(500), + )?; + assert_eq!( + observation.kind(), + WebDriverBiDiWebSocketTransportClosureKind::PeerCloseFrame + ); + assert_eq!(observation.peer_close_status_code(), Some(1000)); + Ok(observation) } #[test] -fn every_missing_operational_observation_keeps_teardown_pending() -> Result<(), Box> { +fn missing_typed_transport_or_process_profile_observation_keeps_teardown_pending() +-> Result<(), Box> { let incomplete_observations = [ (false, true, true), (true, false, true), @@ -129,11 +148,17 @@ fn every_missing_operational_observation_keeps_teardown_pending() -> Result<(), ]; for (transport_closed, browser_exited, profile_removed) in incomplete_observations { - let acknowledged = correlated_session_end_ack()?; + let (acknowledged, established) = correlated_session_end_ack_and_transport()?; + let transport_closure = if transport_closed { + Some(observed_transport_closure(established)?) + } else { + drop(established); + None + }; let assessment = WebDriverBiDiSessionTeardownAssessment::from_protocol_ack( acknowledged, WebDriverBiDiSessionTeardownObservations::new( - transport_closed, + transport_closure, browser_exited, profile_removed, ), @@ -144,6 +169,13 @@ fn every_missing_operational_observation_keeps_teardown_pending() -> Result<(), assessment.observations().transport_closed_observed(), transport_closed ); + assert_eq!( + assessment + .observations() + .transport_closure_observation() + .map(WebDriverBiDiWebSocketTransportClosureObservation::kind), + transport_closed.then_some(WebDriverBiDiWebSocketTransportClosureKind::PeerCloseFrame) + ); assert_eq!( assessment.observations().browser_process_exited_observed(), browser_exited @@ -162,11 +194,13 @@ fn every_missing_operational_observation_keeps_teardown_pending() -> Result<(), } #[test] -fn correlated_ack_plus_all_operational_observations_is_complete() -> Result<(), Box> { - let acknowledged = correlated_session_end_ack()?; +fn correlated_ack_plus_typed_transport_and_all_other_observations_is_complete() +-> Result<(), Box> { + let (acknowledged, established) = correlated_session_end_ack_and_transport()?; + let transport_closure = observed_transport_closure(established)?; let assessment = WebDriverBiDiSessionTeardownAssessment::from_protocol_ack( acknowledged, - WebDriverBiDiSessionTeardownObservations::new(true, true, true), + WebDriverBiDiSessionTeardownObservations::new(Some(transport_closure), true, true), ); assert!(assessment.is_operationally_complete()); @@ -175,5 +209,12 @@ fn correlated_ack_plus_all_operational_observations_is_complete() -> Result<(), WebDriverBiDiSessionTeardownDisposition::OperationallyComplete ); assert_eq!(assessment.command_id(), 7); + assert_eq!( + assessment + .observations() + .transport_closure_observation() + .map(WebDriverBiDiWebSocketTransportClosureObservation::peer_close_status_code), + Some(Some(1000)) + ); Ok(()) } From baba701ac802f12170b2ff469f900ffd3a3e6859 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 30 Aug 2026 09:19:33 -0700 Subject: [PATCH 018/105] style(network): apply canonical rustfmt to typed teardown regression --- .../webdriver_bidi_session_teardown_assessment.rs | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/crates/originweave-network/tests/webdriver_bidi_session_teardown_assessment.rs b/crates/originweave-network/tests/webdriver_bidi_session_teardown_assessment.rs index 14e51e3dc..f3f9baefc 100644 --- a/crates/originweave-network/tests/webdriver_bidi_session_teardown_assessment.rs +++ b/crates/originweave-network/tests/webdriver_bidi_session_teardown_assessment.rs @@ -66,8 +66,13 @@ fn read_masked_text_frame(stream: &mut TcpStream) -> io::Result> { Ok(payload) } -fn correlated_session_end_ack_and_transport() --> Result<(WebDriverBiDiSessionEndResult, WebDriverBiDiWebSocketEstablished), Box> { +fn correlated_session_end_ack_and_transport() -> Result< + ( + WebDriverBiDiSessionEndResult, + WebDriverBiDiWebSocketEstablished, + ), + Box, +> { let listener = TcpListener::bind(("127.0.0.1", 0))?; let local_addr = listener.local_addr()?; let server = thread::spawn(move || -> io::Result<()> { @@ -115,8 +120,7 @@ fn correlated_session_end_ack_and_transport() .into()); } }; - let acknowledged = - WebDriverBiDiSessionEndResult::parse_and_correlate(&text, &mut correlation)?; + let acknowledged = WebDriverBiDiSessionEndResult::parse_and_correlate(&text, &mut correlation)?; server .join() .map_err(|_| io::Error::other("session.end teardown test server panicked"))??; From 4d43cba88ba54ddfe74e52ed1e8906aa1f18eb90 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 30 Aug 2026 09:33:49 -0700 Subject: [PATCH 019/105] feat(network): bind typed transport closure to teardown --- .../src/webdriver_bidi_session_teardown.rs | 44 +++++++++++++------ 1 file changed, 30 insertions(+), 14 deletions(-) diff --git a/crates/originweave-network/src/webdriver_bidi_session_teardown.rs b/crates/originweave-network/src/webdriver_bidi_session_teardown.rs index d214f4c0a..5bff13c3f 100644 --- a/crates/originweave-network/src/webdriver_bidi_session_teardown.rs +++ b/crates/originweave-network/src/webdriver_bidi_session_teardown.rs @@ -1,4 +1,6 @@ -use crate::WebDriverBiDiSessionEndResult; +use crate::{ + WebDriverBiDiSessionEndResult, WebDriverBiDiWebSocketTransportClosureObservation, +}; /// Fail-closed operational disposition derived from explicit teardown observations. /// @@ -15,35 +17,48 @@ pub enum WebDriverBiDiSessionTeardownDisposition { /// Explicit operational observations required after a correlated WebDriver BiDi `session.end` ack. /// -/// These booleans are deliberately observation facts, not authority or evidence provenance. The -/// trusted browser/process/profile owner remains responsible for producing and authenticating the -/// underlying observations before constructing this value. -#[derive(Clone, Copy, Debug, Eq, PartialEq)] +/// Transport closure is represented by the typed observation produced only by consuming the exact +/// established WebSocket at its bounded closure-observation boundary. Browser-process exit and task +/// profile removal remain caller-supplied observation facts until their own typed runtime owners are +/// connected. None of these fields grants authority or authenticates the owning runtime boundary. +#[derive(Debug, Eq, PartialEq)] pub struct WebDriverBiDiSessionTeardownObservations { - transport_closed_observed: bool, + transport_closure_observation: Option, browser_process_exited_observed: bool, task_profile_removed_observed: bool, } impl WebDriverBiDiSessionTeardownObservations { /// Construct the three explicit operational observations required by this boundary. + /// + /// Transport closure cannot be asserted with a caller-supplied boolean. `Some` requires the + /// typed closure observation returned by the bounded transport owner; `None` keeps teardown + /// pending. The remaining booleans are observation placeholders for later typed runtime owners. #[must_use] pub const fn new( - transport_closed_observed: bool, + transport_closure_observation: Option, browser_process_exited_observed: bool, task_profile_removed_observed: bool, ) -> Self { Self { - transport_closed_observed, + transport_closure_observation, browser_process_exited_observed, task_profile_removed_observed, } } - /// Return whether closure of the exact session transport was observed. + /// Return whether typed closure evidence for the exact session transport was supplied. #[must_use] pub const fn transport_closed_observed(&self) -> bool { - self.transport_closed_observed + self.transport_closure_observation.is_some() + } + + /// Borrow the typed transport-closure observation when one was supplied. + #[must_use] + pub const fn transport_closure_observation( + &self, + ) -> Option<&WebDriverBiDiWebSocketTransportClosureObservation> { + self.transport_closure_observation.as_ref() } /// Return whether exit of the owned browser process was observed. @@ -59,7 +74,7 @@ impl WebDriverBiDiSessionTeardownObservations { } const fn operationally_complete(&self) -> bool { - self.transport_closed_observed + self.transport_closure_observation.is_some() & self.browser_process_exited_observed & self.task_profile_removed_observed } @@ -68,9 +83,10 @@ impl WebDriverBiDiSessionTeardownObservations { /// One correlated `session.end` acknowledgment kept separate from operational teardown evidence. /// /// A protocol acknowledgment alone is never operational completion. Callers must separately -/// provide all reviewed transport/process/profile observations, and the observations themselves -/// remain non-authoritative until authenticated by their owning runtime boundary. -#[derive(Clone, Copy, Debug, Eq, PartialEq)] +/// provide all reviewed transport/process/profile observations. Transport closure is retained as a +/// typed observation from the consumed WebSocket, while process/profile observations remain +/// non-authoritative until authenticated by their owning runtime boundaries. +#[derive(Debug, Eq, PartialEq)] pub struct WebDriverBiDiSessionTeardownAssessment { protocol_ack: WebDriverBiDiSessionEndResult, observations: WebDriverBiDiSessionTeardownObservations, From 7d723707765567b71ffd6f99f60cdea5b806a9f7 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 30 Aug 2026 09:39:11 -0700 Subject: [PATCH 020/105] style(network): apply canonical teardown formatting --- .../src/webdriver_bidi_session_teardown.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/crates/originweave-network/src/webdriver_bidi_session_teardown.rs b/crates/originweave-network/src/webdriver_bidi_session_teardown.rs index 5bff13c3f..7184138b9 100644 --- a/crates/originweave-network/src/webdriver_bidi_session_teardown.rs +++ b/crates/originweave-network/src/webdriver_bidi_session_teardown.rs @@ -1,6 +1,4 @@ -use crate::{ - WebDriverBiDiSessionEndResult, WebDriverBiDiWebSocketTransportClosureObservation, -}; +use crate::{WebDriverBiDiSessionEndResult, WebDriverBiDiWebSocketTransportClosureObservation}; /// Fail-closed operational disposition derived from explicit teardown observations. /// From ef1b4a901a4f9871a3c154c328d9da9911a66e9b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 30 Aug 2026 10:04:20 -0700 Subject: [PATCH 021/105] test(network): reject forgeable teardown completion --- .../tests/webdriver_bidi_session_teardown_assessment.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/originweave-network/tests/webdriver_bidi_session_teardown_assessment.rs b/crates/originweave-network/tests/webdriver_bidi_session_teardown_assessment.rs index f3f9baefc..af3fe6787 100644 --- a/crates/originweave-network/tests/webdriver_bidi_session_teardown_assessment.rs +++ b/crates/originweave-network/tests/webdriver_bidi_session_teardown_assessment.rs @@ -198,7 +198,7 @@ fn missing_typed_transport_or_process_profile_observation_keeps_teardown_pending } #[test] -fn correlated_ack_plus_typed_transport_and_all_other_observations_is_complete() +fn caller_asserted_process_and_profile_placeholders_cannot_complete_teardown() -> Result<(), Box> { let (acknowledged, established) = correlated_session_end_ack_and_transport()?; let transport_closure = observed_transport_closure(established)?; @@ -207,10 +207,10 @@ fn correlated_ack_plus_typed_transport_and_all_other_observations_is_complete() WebDriverBiDiSessionTeardownObservations::new(Some(transport_closure), true, true), ); - assert!(assessment.is_operationally_complete()); + assert!(!assessment.is_operationally_complete()); assert_eq!( assessment.disposition(), - WebDriverBiDiSessionTeardownDisposition::OperationallyComplete + WebDriverBiDiSessionTeardownDisposition::OperationalTeardownPending ); assert_eq!(assessment.command_id(), 7); assert_eq!( From 72449de1bfbf156448052448b9046595a5863ec2 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 30 Aug 2026 10:12:30 -0700 Subject: [PATCH 022/105] fix(network): fail closed on teardown placeholders --- .../src/webdriver_bidi_session_teardown.rs | 63 ++++++++++--------- 1 file changed, 32 insertions(+), 31 deletions(-) diff --git a/crates/originweave-network/src/webdriver_bidi_session_teardown.rs b/crates/originweave-network/src/webdriver_bidi_session_teardown.rs index 7184138b9..95a56806d 100644 --- a/crates/originweave-network/src/webdriver_bidi_session_teardown.rs +++ b/crates/originweave-network/src/webdriver_bidi_session_teardown.rs @@ -1,24 +1,24 @@ use crate::{WebDriverBiDiSessionEndResult, WebDriverBiDiWebSocketTransportClosureObservation}; -/// Fail-closed operational disposition derived from explicit teardown observations. +/// Fail-closed operational disposition derived from teardown observations. /// -/// This value does not authenticate any observation or grant process, profile, browser, network, -/// policy, or Agent authority. `OperationallyComplete` means only that the caller supplied all -/// reviewed observation classes after a correlated `session.end` protocol acknowledgment. +/// The current assessment can report only `OperationalTeardownPending`. Transport closure already +/// has a typed owner, but browser-process exit and task-profile removal are still caller-supplied +/// placeholders. Those placeholders cannot establish operational completion or grant process, +/// profile, browser, network, policy, or Agent authority. #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub enum WebDriverBiDiSessionTeardownDisposition { - /// One or more required operational teardown observations remain absent. + /// Operational teardown is not yet proven by typed observations from every owning boundary. OperationalTeardownPending, - /// Every required operational teardown observation was supplied. - OperationallyComplete, } -/// Explicit operational observations required after a correlated WebDriver BiDi `session.end` ack. +/// Explicit operational observations retained after a correlated WebDriver BiDi `session.end` ack. /// /// Transport closure is represented by the typed observation produced only by consuming the exact /// established WebSocket at its bounded closure-observation boundary. Browser-process exit and task -/// profile removal remain caller-supplied observation facts until their own typed runtime owners are -/// connected. None of these fields grants authority or authenticates the owning runtime boundary. +/// profile removal remain caller-supplied observation placeholders until their own typed runtime +/// owners are connected. Placeholder booleans are retained for diagnostics only and can never make +/// this assessment operationally complete. #[derive(Debug, Eq, PartialEq)] pub struct WebDriverBiDiSessionTeardownObservations { transport_closure_observation: Option, @@ -27,11 +27,12 @@ pub struct WebDriverBiDiSessionTeardownObservations { } impl WebDriverBiDiSessionTeardownObservations { - /// Construct the three explicit operational observations required by this boundary. + /// Construct the three explicit operational observations retained by this boundary. /// /// Transport closure cannot be asserted with a caller-supplied boolean. `Some` requires the /// typed closure observation returned by the bounded transport owner; `None` keeps teardown - /// pending. The remaining booleans are observation placeholders for later typed runtime owners. + /// pending. The remaining booleans are diagnostic placeholders for later typed runtime owners + /// and cannot establish operational completion. #[must_use] pub const fn new( transport_closure_observation: Option, @@ -59,31 +60,29 @@ impl WebDriverBiDiSessionTeardownObservations { self.transport_closure_observation.as_ref() } - /// Return whether exit of the owned browser process was observed. + /// Return the caller-supplied browser-process-exit placeholder observation. + /// + /// This value is diagnostic only until a typed process-supervision owner replaces it. #[must_use] pub const fn browser_process_exited_observed(&self) -> bool { self.browser_process_exited_observed } - /// Return whether removal of the owned task profile was observed. + /// Return the caller-supplied task-profile-removal placeholder observation. + /// + /// This value is diagnostic only until a typed profile-lifecycle owner replaces it. #[must_use] pub const fn task_profile_removed_observed(&self) -> bool { self.task_profile_removed_observed } - - const fn operationally_complete(&self) -> bool { - self.transport_closure_observation.is_some() - & self.browser_process_exited_observed - & self.task_profile_removed_observed - } } /// One correlated `session.end` acknowledgment kept separate from operational teardown evidence. /// -/// A protocol acknowledgment alone is never operational completion. Callers must separately -/// provide all reviewed transport/process/profile observations. Transport closure is retained as a -/// typed observation from the consumed WebSocket, while process/profile observations remain -/// non-authoritative until authenticated by their owning runtime boundaries. +/// A protocol acknowledgment alone is never operational completion. Typed transport closure is +/// retained from the consumed WebSocket, while process/profile observations remain non-authoritative +/// placeholders. Consequently this assessment remains fail closed until those owning runtime +/// boundaries supply typed evidence in a later dependency-ordered slice. #[derive(Debug, Eq, PartialEq)] pub struct WebDriverBiDiSessionTeardownAssessment { protocol_ack: WebDriverBiDiSessionEndResult, @@ -115,19 +114,21 @@ impl WebDriverBiDiSessionTeardownAssessment { &self.observations } - /// Return whether all required operational teardown observations are present. + /// Return whether typed evidence proves every required operational teardown boundary. + /// + /// This is always `false` while browser-process exit and task-profile removal remain + /// caller-supplied placeholders rather than typed evidence from their owning runtime boundaries. #[must_use] pub const fn is_operationally_complete(&self) -> bool { - self.observations.operationally_complete() + false } /// Return the fail-closed disposition for the currently supplied observations. + /// + /// The current boundary cannot emit an operational-completion disposition because process and + /// profile cleanup still lack typed owner evidence. #[must_use] pub const fn disposition(&self) -> WebDriverBiDiSessionTeardownDisposition { - if self.is_operationally_complete() { - WebDriverBiDiSessionTeardownDisposition::OperationallyComplete - } else { - WebDriverBiDiSessionTeardownDisposition::OperationalTeardownPending - } + WebDriverBiDiSessionTeardownDisposition::OperationalTeardownPending } } From ce7c40ee7a0e90c2e8e90b27105865282e0db8db Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 30 Aug 2026 13:53:22 -0700 Subject: [PATCH 023/105] test(network): reject process profile teardown placeholders --- ...driver_bidi_session_teardown_assessment.rs | 29 ++++--------------- 1 file changed, 5 insertions(+), 24 deletions(-) diff --git a/crates/originweave-network/tests/webdriver_bidi_session_teardown_assessment.rs b/crates/originweave-network/tests/webdriver_bidi_session_teardown_assessment.rs index af3fe6787..525dcb85a 100644 --- a/crates/originweave-network/tests/webdriver_bidi_session_teardown_assessment.rs +++ b/crates/originweave-network/tests/webdriver_bidi_session_teardown_assessment.rs @@ -143,15 +143,8 @@ fn observed_transport_closure( } #[test] -fn missing_typed_transport_or_process_profile_observation_keeps_teardown_pending() --> Result<(), Box> { - let incomplete_observations = [ - (false, true, true), - (true, false, true), - (true, true, false), - ]; - - for (transport_closed, browser_exited, profile_removed) in incomplete_observations { +fn missing_typed_transport_observation_keeps_teardown_pending() -> Result<(), Box> { + for transport_closed in [false, true] { let (acknowledged, established) = correlated_session_end_ack_and_transport()?; let transport_closure = if transport_closed { Some(observed_transport_closure(established)?) @@ -161,11 +154,7 @@ fn missing_typed_transport_or_process_profile_observation_keeps_teardown_pending }; let assessment = WebDriverBiDiSessionTeardownAssessment::from_protocol_ack( acknowledged, - WebDriverBiDiSessionTeardownObservations::new( - transport_closure, - browser_exited, - profile_removed, - ), + WebDriverBiDiSessionTeardownObservations::new(transport_closure), ); assert_eq!(assessment.command_id(), 7); @@ -180,14 +169,6 @@ fn missing_typed_transport_or_process_profile_observation_keeps_teardown_pending .map(WebDriverBiDiWebSocketTransportClosureObservation::kind), transport_closed.then_some(WebDriverBiDiWebSocketTransportClosureKind::PeerCloseFrame) ); - assert_eq!( - assessment.observations().browser_process_exited_observed(), - browser_exited - ); - assert_eq!( - assessment.observations().task_profile_removed_observed(), - profile_removed - ); assert!(!assessment.is_operationally_complete()); assert_eq!( assessment.disposition(), @@ -198,13 +179,13 @@ fn missing_typed_transport_or_process_profile_observation_keeps_teardown_pending } #[test] -fn caller_asserted_process_and_profile_placeholders_cannot_complete_teardown() +fn typed_transport_closure_cannot_complete_teardown_without_process_and_profile_evidence() -> Result<(), Box> { let (acknowledged, established) = correlated_session_end_ack_and_transport()?; let transport_closure = observed_transport_closure(established)?; let assessment = WebDriverBiDiSessionTeardownAssessment::from_protocol_ack( acknowledged, - WebDriverBiDiSessionTeardownObservations::new(Some(transport_closure), true, true), + WebDriverBiDiSessionTeardownObservations::new(Some(transport_closure)), ); assert!(!assessment.is_operationally_complete()); From 43c9ae7f8cef47975701122cffe135f27f490a17 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 1 Sep 2026 04:05:11 +0900 Subject: [PATCH 024/105] fix(network): remove forgeable teardown placeholders --- .../src/webdriver_bidi_session_teardown.rs | 49 +++++-------------- 1 file changed, 13 insertions(+), 36 deletions(-) diff --git a/crates/originweave-network/src/webdriver_bidi_session_teardown.rs b/crates/originweave-network/src/webdriver_bidi_session_teardown.rs index 95a56806d..166eaa09b 100644 --- a/crates/originweave-network/src/webdriver_bidi_session_teardown.rs +++ b/crates/originweave-network/src/webdriver_bidi_session_teardown.rs @@ -3,9 +3,9 @@ use crate::{WebDriverBiDiSessionEndResult, WebDriverBiDiWebSocketTransportClosur /// Fail-closed operational disposition derived from teardown observations. /// /// The current assessment can report only `OperationalTeardownPending`. Transport closure already -/// has a typed owner, but browser-process exit and task-profile removal are still caller-supplied -/// placeholders. Those placeholders cannot establish operational completion or grant process, -/// profile, browser, network, policy, or Agent authority. +/// has a typed owner, while browser-process exit and task-profile removal are not represented until +/// their own typed runtime owners are connected. Missing owner evidence cannot establish operational +/// completion or grant process, profile, browser, network, policy, secret, or Agent authority. #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub enum WebDriverBiDiSessionTeardownDisposition { /// Operational teardown is not yet proven by typed observations from every owning boundary. @@ -16,33 +16,26 @@ pub enum WebDriverBiDiSessionTeardownDisposition { /// /// Transport closure is represented by the typed observation produced only by consuming the exact /// established WebSocket at its bounded closure-observation boundary. Browser-process exit and task -/// profile removal remain caller-supplied observation placeholders until their own typed runtime -/// owners are connected. Placeholder booleans are retained for diagnostics only and can never make -/// this assessment operationally complete. +/// profile removal are deliberately absent until their own typed runtime owners can supply +/// non-forgeable evidence. #[derive(Debug, Eq, PartialEq)] pub struct WebDriverBiDiSessionTeardownObservations { transport_closure_observation: Option, - browser_process_exited_observed: bool, - task_profile_removed_observed: bool, } impl WebDriverBiDiSessionTeardownObservations { - /// Construct the three explicit operational observations retained by this boundary. + /// Construct the typed transport observation retained by this boundary. /// /// Transport closure cannot be asserted with a caller-supplied boolean. `Some` requires the /// typed closure observation returned by the bounded transport owner; `None` keeps teardown - /// pending. The remaining booleans are diagnostic placeholders for later typed runtime owners - /// and cannot establish operational completion. + /// pending. Process and profile state cannot be supplied as placeholders and remain unproven + /// until their typed runtime owners are connected. #[must_use] pub const fn new( transport_closure_observation: Option, - browser_process_exited_observed: bool, - task_profile_removed_observed: bool, ) -> Self { Self { transport_closure_observation, - browser_process_exited_observed, - task_profile_removed_observed, } } @@ -59,30 +52,14 @@ impl WebDriverBiDiSessionTeardownObservations { ) -> Option<&WebDriverBiDiWebSocketTransportClosureObservation> { self.transport_closure_observation.as_ref() } - - /// Return the caller-supplied browser-process-exit placeholder observation. - /// - /// This value is diagnostic only until a typed process-supervision owner replaces it. - #[must_use] - pub const fn browser_process_exited_observed(&self) -> bool { - self.browser_process_exited_observed - } - - /// Return the caller-supplied task-profile-removal placeholder observation. - /// - /// This value is diagnostic only until a typed profile-lifecycle owner replaces it. - #[must_use] - pub const fn task_profile_removed_observed(&self) -> bool { - self.task_profile_removed_observed - } } /// One correlated `session.end` acknowledgment kept separate from operational teardown evidence. /// /// A protocol acknowledgment alone is never operational completion. Typed transport closure is -/// retained from the consumed WebSocket, while process/profile observations remain non-authoritative -/// placeholders. Consequently this assessment remains fail closed until those owning runtime -/// boundaries supply typed evidence in a later dependency-ordered slice. +/// retained from the consumed WebSocket, while process/profile evidence remains absent until its +/// owning runtime boundaries can provide typed observations. Consequently this assessment remains +/// fail closed in the current dependency-ordered slice. #[derive(Debug, Eq, PartialEq)] pub struct WebDriverBiDiSessionTeardownAssessment { protocol_ack: WebDriverBiDiSessionEndResult, @@ -116,8 +93,8 @@ impl WebDriverBiDiSessionTeardownAssessment { /// Return whether typed evidence proves every required operational teardown boundary. /// - /// This is always `false` while browser-process exit and task-profile removal remain - /// caller-supplied placeholders rather than typed evidence from their owning runtime boundaries. + /// This is always `false` while typed browser-process-exit and task-profile-removal evidence is + /// absent from this boundary. #[must_use] pub const fn is_operationally_complete(&self) -> bool { false From 626026c5037ee23e5b435bd864e7ad4280da5643 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 3 Sep 2026 02:43:24 +0900 Subject: [PATCH 025/105] fix(network): repair typed teardown exact-head regression --- ...ver_bidi_json_envelope_public_boundary_tests.rs | 14 +++++++------- .../src/webdriver_bidi_session_status_response.rs | 8 +++++--- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/crates/originweave-network/src/webdriver_bidi_json_envelope_public_boundary_tests.rs b/crates/originweave-network/src/webdriver_bidi_json_envelope_public_boundary_tests.rs index 6b6487cb0..879fb32bd 100644 --- a/crates/originweave-network/src/webdriver_bidi_json_envelope_public_boundary_tests.rs +++ b/crates/originweave-network/src/webdriver_bidi_json_envelope_public_boundary_tests.rs @@ -9,12 +9,12 @@ use std::{ use originweave_core::WebDriverBiDiWebSocketEndpoint; use crate::{ - WebDriverBiDiCommandCorrelation, WebDriverBiDiJsonEnvelope, WebDriverBiDiJsonEnvelopeError, - WebDriverBiDiJsonEnvelopeKind, WebDriverBiDiSessionStatusResponseError, - WebDriverBiDiSessionStatusResult, WebDriverBiDiTcpConnectionPlan, - WebDriverBiDiWebSocketClientKey, WebDriverBiDiWebSocketHandshakePlan, - WebDriverBiDiWebSocketMessageAssembler, WebDriverBiDiWebSocketMessageAssembly, - WebDriverBiDiWebSocketTextMessage, + WebDriverBiDiCommandCorrelation, WebDriverBiDiCommandKind, WebDriverBiDiJsonEnvelope, + WebDriverBiDiJsonEnvelopeError, WebDriverBiDiJsonEnvelopeKind, + WebDriverBiDiSessionStatusResponseError, WebDriverBiDiSessionStatusResult, + WebDriverBiDiTcpConnectionPlan, WebDriverBiDiWebSocketClientKey, + WebDriverBiDiWebSocketHandshakePlan, WebDriverBiDiWebSocketMessageAssembler, + WebDriverBiDiWebSocketMessageAssembly, WebDriverBiDiWebSocketTextMessage, }; const SESSION_ID: &str = "01234567-89ab-cdef-0123-456789abcdef"; @@ -134,7 +134,7 @@ fn public_json_envelope_unit_build_covers_fail_closed_json_edges() -> Result<(), fn public_session_status_empty_result_fails_closed_from_unit_build() -> Result<(), Box> { let text = read_text_over_loopback(EMPTY_STATUS_RESULT)?; let mut correlation = WebDriverBiDiCommandCorrelation::new(); - correlation.register_command(7)?; + correlation.register_command_for(7, WebDriverBiDiCommandKind::SessionStatus)?; let parsed = WebDriverBiDiSessionStatusResult::parse_and_correlate(&text, &mut correlation); assert!(matches!( diff --git a/crates/originweave-network/src/webdriver_bidi_session_status_response.rs b/crates/originweave-network/src/webdriver_bidi_session_status_response.rs index e08a7efbb..b1e5596ba 100644 --- a/crates/originweave-network/src/webdriver_bidi_session_status_response.rs +++ b/crates/originweave-network/src/webdriver_bidi_session_status_response.rs @@ -71,9 +71,11 @@ impl WebDriverBiDiSessionStatusResult { retain_validated_error_code(envelope.error_code()).and_then(|error_code| { let completed = correlation .correlate_response_for(&envelope, WebDriverBiDiCommandKind::SessionStatus) - .map_err(|source| { - WebDriverBiDiSessionStatusResponseError::Correlation { source } - })?; + .map_err( + |source| WebDriverBiDiSessionStatusResponseError::Correlation { + source, + }, + )?; Err( WebDriverBiDiSessionStatusResponseError::RemoteProtocolError { command_id: completed.command_id(), From 88e68f37d655ae62dae6a38a50c12b30e2801160 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 4 Sep 2026 00:32:24 +0900 Subject: [PATCH 026/105] fix(network): encode validated BiDi response routing structurally Remove the impossible success-without-command-id correlation state exposed by exact production coverage. Keep success ids structurally present after envelope validation, nullable ids only on protocol errors, and events id-less while preserving the public envelope API. No coverage exclusion, threshold change, workflow mutation, or authority widening. --- crates/originweave-network/src/lib.rs | 1 + .../src/webdriver_bidi_command_correlation.rs | 41 ++++++++-------- .../src/webdriver_bidi_json_envelope.rs | 48 ++++++++++++++----- 3 files changed, 56 insertions(+), 34 deletions(-) diff --git a/crates/originweave-network/src/lib.rs b/crates/originweave-network/src/lib.rs index 1d3fc2d24..1ff23ce4d 100644 --- a/crates/originweave-network/src/lib.rs +++ b/crates/originweave-network/src/lib.rs @@ -48,6 +48,7 @@ pub use webdriver_bidi_connection::{ WebDriverBiDiTcpConnection, WebDriverBiDiTcpConnectionError, WebDriverBiDiTcpConnectionEvidence, WebDriverBiDiTcpConnectionPlan, }; +pub(crate) use webdriver_bidi_json_envelope::WebDriverBiDiJsonEnvelopeRouting; pub use webdriver_bidi_json_envelope::{ MAX_WEBDRIVER_BIDI_JS_UINT, MAX_WEBDRIVER_BIDI_JSON_DEPTH, WebDriverBiDiJsonEnvelope, WebDriverBiDiJsonEnvelopeError, WebDriverBiDiJsonEnvelopeKind, diff --git a/crates/originweave-network/src/webdriver_bidi_command_correlation.rs b/crates/originweave-network/src/webdriver_bidi_command_correlation.rs index 4dd9bcd78..43d5e911e 100644 --- a/crates/originweave-network/src/webdriver_bidi_command_correlation.rs +++ b/crates/originweave-network/src/webdriver_bidi_command_correlation.rs @@ -1,6 +1,8 @@ use std::{collections::BTreeMap, error::Error, fmt}; -use crate::{MAX_WEBDRIVER_BIDI_JS_UINT, WebDriverBiDiJsonEnvelope, WebDriverBiDiJsonEnvelopeKind}; +use crate::{ + MAX_WEBDRIVER_BIDI_JS_UINT, WebDriverBiDiJsonEnvelope, WebDriverBiDiJsonEnvelopeRouting, +}; /// Maximum number of local WebDriver BiDi commands retained as outstanding at once. /// @@ -183,30 +185,25 @@ impl WebDriverBiDiCommandCorrelation { envelope: &WebDriverBiDiJsonEnvelope, expected_kind: WebDriverBiDiCommandKind, ) -> Result { - match envelope.kind() { - WebDriverBiDiJsonEnvelopeKind::Event => { + match envelope.routing() { + WebDriverBiDiJsonEnvelopeRouting::Event => { Err(WebDriverBiDiCommandCorrelationError::EventIsNotResponse) } - WebDriverBiDiJsonEnvelopeKind::Error => { - let Some(command_id) = envelope.command_id() else { - return Err(WebDriverBiDiCommandCorrelationError::UncorrelatableErrorResponse); - }; - self.complete( - command_id, - expected_kind, - WebDriverBiDiCorrelatedResponseOutcome::Error, - ) - } - WebDriverBiDiJsonEnvelopeKind::Success => { - let Some(command_id) = envelope.command_id() else { - return Err(WebDriverBiDiCommandCorrelationError::CommandNotOutstanding); - }; - self.complete( - command_id, - expected_kind, - WebDriverBiDiCorrelatedResponseOutcome::Success, - ) + WebDriverBiDiJsonEnvelopeRouting::CommandError { command_id: None } => { + Err(WebDriverBiDiCommandCorrelationError::UncorrelatableErrorResponse) } + WebDriverBiDiJsonEnvelopeRouting::CommandError { + command_id: Some(command_id), + } => self.complete( + command_id, + expected_kind, + WebDriverBiDiCorrelatedResponseOutcome::Error, + ), + WebDriverBiDiJsonEnvelopeRouting::CommandSuccess { command_id } => self.complete( + command_id, + expected_kind, + WebDriverBiDiCorrelatedResponseOutcome::Success, + ), } } diff --git a/crates/originweave-network/src/webdriver_bidi_json_envelope.rs b/crates/originweave-network/src/webdriver_bidi_json_envelope.rs index e99adf2c9..7e878980f 100644 --- a/crates/originweave-network/src/webdriver_bidi_json_envelope.rs +++ b/crates/originweave-network/src/webdriver_bidi_json_envelope.rs @@ -23,6 +23,18 @@ pub enum WebDriverBiDiJsonEnvelopeKind { Event, } +/// Structurally valid command/event routing retained after common-envelope validation. +/// +/// Keeping success ids inside the success variant prevents an impossible `success` + missing-id +/// state from leaking into downstream command correlation. Error ids remain optional because the +/// WebDriver BiDi protocol explicitly permits `null` there, while events carry no command id. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub(crate) enum WebDriverBiDiJsonEnvelopeRouting { + CommandSuccess { command_id: u64 }, + CommandError { command_id: Option }, + Event, +} + /// Credential-minimal classification of one complete WebDriver BiDi local-end JSON envelope. /// /// Result and parameter bodies are deliberately validated and discarded at this boundary. They @@ -30,8 +42,7 @@ pub enum WebDriverBiDiJsonEnvelopeKind { /// as generic JSON values that could become ambient browser or Agent authority. #[derive(Eq, PartialEq)] pub struct WebDriverBiDiJsonEnvelope { - kind: WebDriverBiDiJsonEnvelopeKind, - command_id: Option, + routing: WebDriverBiDiJsonEnvelopeRouting, method: Option, error_code: Option, } @@ -40,8 +51,8 @@ impl fmt::Debug for WebDriverBiDiJsonEnvelope { fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { formatter .debug_struct("WebDriverBiDiJsonEnvelope") - .field("kind", &self.kind) - .field("command_id", &self.command_id) + .field("kind", &self.kind()) + .field("command_id", &self.command_id()) .field("has_method", &self.method.is_some()) .field("has_error_code", &self.error_code.is_some()) .finish() @@ -73,7 +84,15 @@ impl WebDriverBiDiJsonEnvelope { /// Return the classified local-end envelope kind. #[must_use] pub const fn kind(&self) -> WebDriverBiDiJsonEnvelopeKind { - self.kind + match self.routing { + WebDriverBiDiJsonEnvelopeRouting::CommandSuccess { .. } => { + WebDriverBiDiJsonEnvelopeKind::Success + } + WebDriverBiDiJsonEnvelopeRouting::CommandError { .. } => { + WebDriverBiDiJsonEnvelopeKind::Error + } + WebDriverBiDiJsonEnvelopeRouting::Event => WebDriverBiDiJsonEnvelopeKind::Event, + } } /// Return the command identifier for success and correlatable error responses. @@ -81,7 +100,15 @@ impl WebDriverBiDiJsonEnvelope { /// Events and error responses whose protocol `id` is `null` return `None`. #[must_use] pub const fn command_id(&self) -> Option { - self.command_id + match self.routing { + WebDriverBiDiJsonEnvelopeRouting::CommandSuccess { command_id } => Some(command_id), + WebDriverBiDiJsonEnvelopeRouting::CommandError { command_id } => command_id, + WebDriverBiDiJsonEnvelopeRouting::Event => None, + } + } + + pub(crate) const fn routing(&self) -> WebDriverBiDiJsonEnvelopeRouting { + self.routing } /// Borrow the event method when this is an event envelope. @@ -208,8 +235,7 @@ impl TopLevelFields { let command_id = required_js_uint(self.id, "id")?; require_object(self.result, "result")?; Ok(WebDriverBiDiJsonEnvelope { - kind: WebDriverBiDiJsonEnvelopeKind::Success, - command_id: Some(command_id), + routing: WebDriverBiDiJsonEnvelopeRouting::CommandSuccess { command_id }, method: None, error_code: None, }) @@ -223,8 +249,7 @@ impl TopLevelFields { require_text_value(stacktrace, "stacktrace")?; } Ok(WebDriverBiDiJsonEnvelope { - kind: WebDriverBiDiJsonEnvelopeKind::Error, - command_id, + routing: WebDriverBiDiJsonEnvelopeRouting::CommandError { command_id }, method: None, error_code: Some(error_code), }) @@ -234,8 +259,7 @@ impl TopLevelFields { let method = required_text(self.method, "method")?; require_object(self.params, "params")?; Ok(WebDriverBiDiJsonEnvelope { - kind: WebDriverBiDiJsonEnvelopeKind::Event, - command_id: None, + routing: WebDriverBiDiJsonEnvelopeRouting::Event, method: Some(method), error_code: None, }) From ab1dea72cc4148367a59afe3c83fe65d7bb961fb Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 4 Sep 2026 00:34:20 +0900 Subject: [PATCH 027/105] docs(doctoring): refresh WebDriver BiDi published baseline --- docs/doctoring/browser-agent-protocols.md | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/docs/doctoring/browser-agent-protocols.md b/docs/doctoring/browser-agent-protocols.md index dbf3ef731..4cc19b210 100644 --- a/docs/doctoring/browser-agent-protocols.md +++ b/docs/doctoring/browser-agent-protocols.md @@ -1,6 +1,6 @@ # Browser and Agent Protocol Standards Evidence -- **Reviewed:** 2026-08-18 +- **Reviewed:** 2026-09-03 - **Purpose:** primary-source evidence for OriginWeave browser compatibility and adapter boundaries - **Canonical research index:** [`../doctoring.md`](../doctoring.md) @@ -8,15 +8,15 @@ This addendum complements the main doctoring record. The main record already car ## WebDriver BiDi -The latest published W3C technical-report baseline reviewed here remains the 1 June 2026 **Working Draft**, not a Recommendation. The current Editor’s Draft reviewed on 18 August 2026 identifies itself as the 20 July 2026 draft. OriginWeave therefore treats BiDi as a versioned browser-automation adapter rather than product-internal authority. Raw BiDi session/context/node identifiers do not become durable OriginWeave identities. +The latest published W3C technical-report baseline reviewed here is the 18 August 2026 **Working Draft**, not a Recommendation. The current Editor’s Draft is consulted only as moving supplemental evidence; the dated Working Draft is the reproducible standards baseline for this review. OriginWeave therefore treats BiDi as a versioned browser-automation adapter rather than product-internal authority. Raw BiDi session/context/node identifiers do not become durable OriginWeave identities. -For the bounded `browsingContext.locateNodes` command-serialization boundary, the reviewed Editor’s Draft defines a command envelope with `id: js-uint`, defines `js-uint` as `0..9007199254740991`, and defines `browsingContext.locateNodes` parameters containing a browsing context, locator, optional positive `maxNodeCount`, optional `serializationOptions`, and optional `startNodes`. OriginWeave serializes only its separately reviewed accessibility-locator subset and fixed minimal serialization options; this deterministic JSON value is not transport authentication or browser/Agent authority. +For the bounded `browsingContext.locateNodes` command-serialization boundary, the reviewed specification defines a command envelope with `id: js-uint`, defines `js-uint` as `0..9007199254740991`, and defines `browsingContext.locateNodes` parameters containing a browsing context, locator, optional positive `maxNodeCount`, optional `serializationOptions`, and optional `startNodes`. OriginWeave serializes only its separately reviewed accessibility-locator subset and fixed minimal serialization options; this deterministic JSON value is not transport authentication or browser/Agent authority. -WebDriver BiDi commands may execute concurrently and finish out of order. The Editor’s Draft defines the command id as the local end’s correlation identifier and sets a successful `CommandResponse.id` to that exact command id; an `ErrorResponse.id` may be `null` when no valid command id can be recovered. OriginWeave therefore fails closed unless a non-null protocol-range response id exactly matches the consumed command before later payload admission. Parsing success/error envelopes, handling nullable malformed-command errors, and authenticating the browser transport remain separate adapter boundaries. +WebDriver BiDi commands may execute concurrently and finish out of order. The 18 August 2026 Working Draft defines the command id as the local end’s correlation identifier; its local-end `CommandResponse` production requires `id: js-uint`, while `ErrorResponse.id` is `js-uint / null`. OriginWeave therefore represents a validated success response with a structurally present command id, retains nullable ids only for protocol errors, and fails closed unless a non-null protocol-range response id exactly matches the consumed command before later payload admission. Parsing success/error envelopes, handling nullable malformed-command errors, and authenticating the browser transport remain separate adapter boundaries. -The same reviewed Editor’s Draft defines a closed `ErrorCode` vocabulary that currently includes `no such client window`. OriginWeave admits only the reviewed vocabulary at its bounded response-envelope parser and rejects unknown error-code text fail closed; adding a newly reviewed protocol code changes compatibility only and grants no browser, transport, node, policy, or Agent authority. +The same reviewed Working Draft defines a closed `ErrorCode` vocabulary that currently includes `no such client window`. OriginWeave admits only the reviewed vocabulary at its bounded response-envelope parser and rejects unknown error-code text fail closed; adding a newly reviewed protocol code changes compatibility only and grants no browser, transport, node, policy, or Agent authority. -Primary sources: World Wide Web Consortium, *WebDriver BiDi* (published Working Draft and current Editor’s Draft). +Primary sources: World Wide Web Consortium, *WebDriver BiDi* (18 August 2026 Working Draft and current Editor’s Draft). ## Chrome Manifest V3 @@ -83,10 +83,10 @@ World Wide Web Consortium. (2013). *PROV-O: The PROV ontology*. https://www.w3.o World Wide Web Consortium. (2023, June 6). *Accessible Rich Internet Applications (WAI-ARIA) 1.2*. https://www.w3.org/TR/2023/REC-wai-aria-1.2-20230606/ -World Wide Web Consortium. (2026, June 1). *WebDriver BiDi* (W3C Working Draft). https://www.w3.org/TR/2026/WD-webdriver-bidi-20260601/ +World Wide Web Consortium. (2026, August 18). *WebDriver BiDi* (W3C Working Draft). https://www.w3.org/TR/2026/WD-webdriver-bidi-20260818/ -World Wide Web Consortium. (2026, July 20). *WebDriver BiDi* (Editor’s Draft). https://w3c.github.io/webdriver-bidi/ +World Wide Web Consortium. (2026). *WebDriver BiDi* (Editor’s Draft). Retrieved September 3, 2026, from https://w3c.github.io/webdriver-bidi/ World Wide Web Consortium. (2026, August 5). *Accessible name and description computation 1.2* (W3C Working Draft). https://www.w3.org/TR/2026/WD-accname-1.2-20260805/ -International Organization for Standardization. (2017). *Information and documentation—WARC file format* (ISO Standard No. 28500:2017). https://www.iso.org/standard/68004.html +International Organization for Standardization. (2017). *Information and documentation—WARC file format* (ISO Standard No. 28500:2017). https://www.iso.org/standard/68004.html \ No newline at end of file From 870cb39d76748e9a5e632345d81e8e0ce00bc4c2 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 4 Sep 2026 00:36:25 +0900 Subject: [PATCH 028/105] fix(network): make BiDi response routing structurally valid Repair exact-head coverage at the earliest executing teardown-stack owner: a validated success response now carries a structurally present command id, nullable ids remain exclusive to protocol errors, and events are id-less. Refresh browser-protocol doctoring to the W3C WebDriver BiDi 18 August 2026 Working Draft. Preserve public envelope behavior and avoid synthetic coverage or authority widening. --- crates/originweave-network/src/lib.rs | 1 + .../src/webdriver_bidi_command_correlation.rs | 41 ++++++++-------- .../src/webdriver_bidi_json_envelope.rs | 48 ++++++++++++++----- docs/doctoring/browser-agent-protocols.md | 18 +++---- 4 files changed, 65 insertions(+), 43 deletions(-) diff --git a/crates/originweave-network/src/lib.rs b/crates/originweave-network/src/lib.rs index 1d3fc2d24..1ff23ce4d 100644 --- a/crates/originweave-network/src/lib.rs +++ b/crates/originweave-network/src/lib.rs @@ -48,6 +48,7 @@ pub use webdriver_bidi_connection::{ WebDriverBiDiTcpConnection, WebDriverBiDiTcpConnectionError, WebDriverBiDiTcpConnectionEvidence, WebDriverBiDiTcpConnectionPlan, }; +pub(crate) use webdriver_bidi_json_envelope::WebDriverBiDiJsonEnvelopeRouting; pub use webdriver_bidi_json_envelope::{ MAX_WEBDRIVER_BIDI_JS_UINT, MAX_WEBDRIVER_BIDI_JSON_DEPTH, WebDriverBiDiJsonEnvelope, WebDriverBiDiJsonEnvelopeError, WebDriverBiDiJsonEnvelopeKind, diff --git a/crates/originweave-network/src/webdriver_bidi_command_correlation.rs b/crates/originweave-network/src/webdriver_bidi_command_correlation.rs index 4dd9bcd78..43d5e911e 100644 --- a/crates/originweave-network/src/webdriver_bidi_command_correlation.rs +++ b/crates/originweave-network/src/webdriver_bidi_command_correlation.rs @@ -1,6 +1,8 @@ use std::{collections::BTreeMap, error::Error, fmt}; -use crate::{MAX_WEBDRIVER_BIDI_JS_UINT, WebDriverBiDiJsonEnvelope, WebDriverBiDiJsonEnvelopeKind}; +use crate::{ + MAX_WEBDRIVER_BIDI_JS_UINT, WebDriverBiDiJsonEnvelope, WebDriverBiDiJsonEnvelopeRouting, +}; /// Maximum number of local WebDriver BiDi commands retained as outstanding at once. /// @@ -183,30 +185,25 @@ impl WebDriverBiDiCommandCorrelation { envelope: &WebDriverBiDiJsonEnvelope, expected_kind: WebDriverBiDiCommandKind, ) -> Result { - match envelope.kind() { - WebDriverBiDiJsonEnvelopeKind::Event => { + match envelope.routing() { + WebDriverBiDiJsonEnvelopeRouting::Event => { Err(WebDriverBiDiCommandCorrelationError::EventIsNotResponse) } - WebDriverBiDiJsonEnvelopeKind::Error => { - let Some(command_id) = envelope.command_id() else { - return Err(WebDriverBiDiCommandCorrelationError::UncorrelatableErrorResponse); - }; - self.complete( - command_id, - expected_kind, - WebDriverBiDiCorrelatedResponseOutcome::Error, - ) - } - WebDriverBiDiJsonEnvelopeKind::Success => { - let Some(command_id) = envelope.command_id() else { - return Err(WebDriverBiDiCommandCorrelationError::CommandNotOutstanding); - }; - self.complete( - command_id, - expected_kind, - WebDriverBiDiCorrelatedResponseOutcome::Success, - ) + WebDriverBiDiJsonEnvelopeRouting::CommandError { command_id: None } => { + Err(WebDriverBiDiCommandCorrelationError::UncorrelatableErrorResponse) } + WebDriverBiDiJsonEnvelopeRouting::CommandError { + command_id: Some(command_id), + } => self.complete( + command_id, + expected_kind, + WebDriverBiDiCorrelatedResponseOutcome::Error, + ), + WebDriverBiDiJsonEnvelopeRouting::CommandSuccess { command_id } => self.complete( + command_id, + expected_kind, + WebDriverBiDiCorrelatedResponseOutcome::Success, + ), } } diff --git a/crates/originweave-network/src/webdriver_bidi_json_envelope.rs b/crates/originweave-network/src/webdriver_bidi_json_envelope.rs index e99adf2c9..7e878980f 100644 --- a/crates/originweave-network/src/webdriver_bidi_json_envelope.rs +++ b/crates/originweave-network/src/webdriver_bidi_json_envelope.rs @@ -23,6 +23,18 @@ pub enum WebDriverBiDiJsonEnvelopeKind { Event, } +/// Structurally valid command/event routing retained after common-envelope validation. +/// +/// Keeping success ids inside the success variant prevents an impossible `success` + missing-id +/// state from leaking into downstream command correlation. Error ids remain optional because the +/// WebDriver BiDi protocol explicitly permits `null` there, while events carry no command id. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub(crate) enum WebDriverBiDiJsonEnvelopeRouting { + CommandSuccess { command_id: u64 }, + CommandError { command_id: Option }, + Event, +} + /// Credential-minimal classification of one complete WebDriver BiDi local-end JSON envelope. /// /// Result and parameter bodies are deliberately validated and discarded at this boundary. They @@ -30,8 +42,7 @@ pub enum WebDriverBiDiJsonEnvelopeKind { /// as generic JSON values that could become ambient browser or Agent authority. #[derive(Eq, PartialEq)] pub struct WebDriverBiDiJsonEnvelope { - kind: WebDriverBiDiJsonEnvelopeKind, - command_id: Option, + routing: WebDriverBiDiJsonEnvelopeRouting, method: Option, error_code: Option, } @@ -40,8 +51,8 @@ impl fmt::Debug for WebDriverBiDiJsonEnvelope { fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { formatter .debug_struct("WebDriverBiDiJsonEnvelope") - .field("kind", &self.kind) - .field("command_id", &self.command_id) + .field("kind", &self.kind()) + .field("command_id", &self.command_id()) .field("has_method", &self.method.is_some()) .field("has_error_code", &self.error_code.is_some()) .finish() @@ -73,7 +84,15 @@ impl WebDriverBiDiJsonEnvelope { /// Return the classified local-end envelope kind. #[must_use] pub const fn kind(&self) -> WebDriverBiDiJsonEnvelopeKind { - self.kind + match self.routing { + WebDriverBiDiJsonEnvelopeRouting::CommandSuccess { .. } => { + WebDriverBiDiJsonEnvelopeKind::Success + } + WebDriverBiDiJsonEnvelopeRouting::CommandError { .. } => { + WebDriverBiDiJsonEnvelopeKind::Error + } + WebDriverBiDiJsonEnvelopeRouting::Event => WebDriverBiDiJsonEnvelopeKind::Event, + } } /// Return the command identifier for success and correlatable error responses. @@ -81,7 +100,15 @@ impl WebDriverBiDiJsonEnvelope { /// Events and error responses whose protocol `id` is `null` return `None`. #[must_use] pub const fn command_id(&self) -> Option { - self.command_id + match self.routing { + WebDriverBiDiJsonEnvelopeRouting::CommandSuccess { command_id } => Some(command_id), + WebDriverBiDiJsonEnvelopeRouting::CommandError { command_id } => command_id, + WebDriverBiDiJsonEnvelopeRouting::Event => None, + } + } + + pub(crate) const fn routing(&self) -> WebDriverBiDiJsonEnvelopeRouting { + self.routing } /// Borrow the event method when this is an event envelope. @@ -208,8 +235,7 @@ impl TopLevelFields { let command_id = required_js_uint(self.id, "id")?; require_object(self.result, "result")?; Ok(WebDriverBiDiJsonEnvelope { - kind: WebDriverBiDiJsonEnvelopeKind::Success, - command_id: Some(command_id), + routing: WebDriverBiDiJsonEnvelopeRouting::CommandSuccess { command_id }, method: None, error_code: None, }) @@ -223,8 +249,7 @@ impl TopLevelFields { require_text_value(stacktrace, "stacktrace")?; } Ok(WebDriverBiDiJsonEnvelope { - kind: WebDriverBiDiJsonEnvelopeKind::Error, - command_id, + routing: WebDriverBiDiJsonEnvelopeRouting::CommandError { command_id }, method: None, error_code: Some(error_code), }) @@ -234,8 +259,7 @@ impl TopLevelFields { let method = required_text(self.method, "method")?; require_object(self.params, "params")?; Ok(WebDriverBiDiJsonEnvelope { - kind: WebDriverBiDiJsonEnvelopeKind::Event, - command_id: None, + routing: WebDriverBiDiJsonEnvelopeRouting::Event, method: Some(method), error_code: None, }) diff --git a/docs/doctoring/browser-agent-protocols.md b/docs/doctoring/browser-agent-protocols.md index dbf3ef731..4cc19b210 100644 --- a/docs/doctoring/browser-agent-protocols.md +++ b/docs/doctoring/browser-agent-protocols.md @@ -1,6 +1,6 @@ # Browser and Agent Protocol Standards Evidence -- **Reviewed:** 2026-08-18 +- **Reviewed:** 2026-09-03 - **Purpose:** primary-source evidence for OriginWeave browser compatibility and adapter boundaries - **Canonical research index:** [`../doctoring.md`](../doctoring.md) @@ -8,15 +8,15 @@ This addendum complements the main doctoring record. The main record already car ## WebDriver BiDi -The latest published W3C technical-report baseline reviewed here remains the 1 June 2026 **Working Draft**, not a Recommendation. The current Editor’s Draft reviewed on 18 August 2026 identifies itself as the 20 July 2026 draft. OriginWeave therefore treats BiDi as a versioned browser-automation adapter rather than product-internal authority. Raw BiDi session/context/node identifiers do not become durable OriginWeave identities. +The latest published W3C technical-report baseline reviewed here is the 18 August 2026 **Working Draft**, not a Recommendation. The current Editor’s Draft is consulted only as moving supplemental evidence; the dated Working Draft is the reproducible standards baseline for this review. OriginWeave therefore treats BiDi as a versioned browser-automation adapter rather than product-internal authority. Raw BiDi session/context/node identifiers do not become durable OriginWeave identities. -For the bounded `browsingContext.locateNodes` command-serialization boundary, the reviewed Editor’s Draft defines a command envelope with `id: js-uint`, defines `js-uint` as `0..9007199254740991`, and defines `browsingContext.locateNodes` parameters containing a browsing context, locator, optional positive `maxNodeCount`, optional `serializationOptions`, and optional `startNodes`. OriginWeave serializes only its separately reviewed accessibility-locator subset and fixed minimal serialization options; this deterministic JSON value is not transport authentication or browser/Agent authority. +For the bounded `browsingContext.locateNodes` command-serialization boundary, the reviewed specification defines a command envelope with `id: js-uint`, defines `js-uint` as `0..9007199254740991`, and defines `browsingContext.locateNodes` parameters containing a browsing context, locator, optional positive `maxNodeCount`, optional `serializationOptions`, and optional `startNodes`. OriginWeave serializes only its separately reviewed accessibility-locator subset and fixed minimal serialization options; this deterministic JSON value is not transport authentication or browser/Agent authority. -WebDriver BiDi commands may execute concurrently and finish out of order. The Editor’s Draft defines the command id as the local end’s correlation identifier and sets a successful `CommandResponse.id` to that exact command id; an `ErrorResponse.id` may be `null` when no valid command id can be recovered. OriginWeave therefore fails closed unless a non-null protocol-range response id exactly matches the consumed command before later payload admission. Parsing success/error envelopes, handling nullable malformed-command errors, and authenticating the browser transport remain separate adapter boundaries. +WebDriver BiDi commands may execute concurrently and finish out of order. The 18 August 2026 Working Draft defines the command id as the local end’s correlation identifier; its local-end `CommandResponse` production requires `id: js-uint`, while `ErrorResponse.id` is `js-uint / null`. OriginWeave therefore represents a validated success response with a structurally present command id, retains nullable ids only for protocol errors, and fails closed unless a non-null protocol-range response id exactly matches the consumed command before later payload admission. Parsing success/error envelopes, handling nullable malformed-command errors, and authenticating the browser transport remain separate adapter boundaries. -The same reviewed Editor’s Draft defines a closed `ErrorCode` vocabulary that currently includes `no such client window`. OriginWeave admits only the reviewed vocabulary at its bounded response-envelope parser and rejects unknown error-code text fail closed; adding a newly reviewed protocol code changes compatibility only and grants no browser, transport, node, policy, or Agent authority. +The same reviewed Working Draft defines a closed `ErrorCode` vocabulary that currently includes `no such client window`. OriginWeave admits only the reviewed vocabulary at its bounded response-envelope parser and rejects unknown error-code text fail closed; adding a newly reviewed protocol code changes compatibility only and grants no browser, transport, node, policy, or Agent authority. -Primary sources: World Wide Web Consortium, *WebDriver BiDi* (published Working Draft and current Editor’s Draft). +Primary sources: World Wide Web Consortium, *WebDriver BiDi* (18 August 2026 Working Draft and current Editor’s Draft). ## Chrome Manifest V3 @@ -83,10 +83,10 @@ World Wide Web Consortium. (2013). *PROV-O: The PROV ontology*. https://www.w3.o World Wide Web Consortium. (2023, June 6). *Accessible Rich Internet Applications (WAI-ARIA) 1.2*. https://www.w3.org/TR/2023/REC-wai-aria-1.2-20230606/ -World Wide Web Consortium. (2026, June 1). *WebDriver BiDi* (W3C Working Draft). https://www.w3.org/TR/2026/WD-webdriver-bidi-20260601/ +World Wide Web Consortium. (2026, August 18). *WebDriver BiDi* (W3C Working Draft). https://www.w3.org/TR/2026/WD-webdriver-bidi-20260818/ -World Wide Web Consortium. (2026, July 20). *WebDriver BiDi* (Editor’s Draft). https://w3c.github.io/webdriver-bidi/ +World Wide Web Consortium. (2026). *WebDriver BiDi* (Editor’s Draft). Retrieved September 3, 2026, from https://w3c.github.io/webdriver-bidi/ World Wide Web Consortium. (2026, August 5). *Accessible name and description computation 1.2* (W3C Working Draft). https://www.w3.org/TR/2026/WD-accname-1.2-20260805/ -International Organization for Standardization. (2017). *Information and documentation—WARC file format* (ISO Standard No. 28500:2017). https://www.iso.org/standard/68004.html +International Organization for Standardization. (2017). *Information and documentation—WARC file format* (ISO Standard No. 28500:2017). https://www.iso.org/standard/68004.html \ No newline at end of file From f816be5ad07e09506049431d805c8f2b1dbd9369 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 5 Sep 2026 15:29:41 +0900 Subject: [PATCH 029/105] test(network): reject cross-connection teardown closure evidence --- ...driver_bidi_session_teardown_assessment.rs | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/crates/originweave-network/tests/webdriver_bidi_session_teardown_assessment.rs b/crates/originweave-network/tests/webdriver_bidi_session_teardown_assessment.rs index 525dcb85a..5a38817ad 100644 --- a/crates/originweave-network/tests/webdriver_bidi_session_teardown_assessment.rs +++ b/crates/originweave-network/tests/webdriver_bidi_session_teardown_assessment.rs @@ -203,3 +203,23 @@ fn typed_transport_closure_cannot_complete_teardown_without_process_and_profile_ ); Ok(()) } + +#[test] +fn closure_from_another_connection_is_not_accepted_for_the_acknowledged_transport() +-> Result<(), Box> { + let (acknowledged_a, established_a) = correlated_session_end_ack_and_transport()?; + let (_acknowledged_b, established_b) = correlated_session_end_ack_and_transport()?; + drop(established_a); + + let closure_b = observed_transport_closure(established_b)?; + let assessment = WebDriverBiDiSessionTeardownAssessment::from_protocol_ack( + acknowledged_a, + WebDriverBiDiSessionTeardownObservations::new(Some(closure_b)), + ); + + assert!( + !assessment.observations().transport_closed_observed(), + "closure from a distinct WebSocket generation must not be attributed to the acknowledged session.end transport" + ); + Ok(()) +} From f1f9664d984cf3149c140718cb0c85481981f736 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 5 Sep 2026 15:32:15 +0900 Subject: [PATCH 030/105] fix(network): fail closed on connection-generation exhaustion --- .../src/webdriver_bidi_connection/error.rs | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/crates/originweave-network/src/webdriver_bidi_connection/error.rs b/crates/originweave-network/src/webdriver_bidi_connection/error.rs index 226cd1d2b..ca8aa41b6 100644 --- a/crates/originweave-network/src/webdriver_bidi_connection/error.rs +++ b/crates/originweave-network/src/webdriver_bidi_connection/error.rs @@ -19,6 +19,8 @@ pub enum WebDriverBiDiTcpConnectionError { /// The largest accepted attempt count. maximum_attempts: u8, }, + /// The process-local connection-generation space was exhausted before a distinct identity could be minted. + ConnectionGenerationExhausted, /// The final bounded connection attempt timed out. ConnectionTimedOut { /// Exact approved socket address submitted to the operating system. @@ -66,7 +68,9 @@ impl WebDriverBiDiTcpConnectionError { | Self::ConnectionFailed { attempt_count, .. } => Some(*attempt_count), Self::PeerInspectionFailed { attempt_number, .. } | Self::PeerMismatch { attempt_number, .. } => Some(*attempt_number), - Self::InvalidConnectTimeout { .. } | Self::InvalidAttemptCount { .. } => None, + Self::InvalidConnectTimeout { .. } + | Self::InvalidAttemptCount { .. } + | Self::ConnectionGenerationExhausted => None, } } } @@ -88,6 +92,8 @@ impl fmt::Display for WebDriverBiDiTcpConnectionError { formatter, "WebDriver BiDi connection attempt count {attempt_count} is outside 1..={maximum_attempts}", ), + Self::ConnectionGenerationExhausted => formatter + .write_str("WebDriver BiDi connection generation space is exhausted"), Self::ConnectionTimedOut { socket_address, attempt_count, @@ -128,7 +134,9 @@ impl std::error::Error for WebDriverBiDiTcpConnectionError { | Self::ConnectionFailed { source, .. } | Self::PeerInspectionFailed { source, .. } => Some(source), Self::PeerMismatch { source, .. } => Some(source), - Self::InvalidConnectTimeout { .. } | Self::InvalidAttemptCount { .. } => None, + Self::InvalidConnectTimeout { .. } + | Self::InvalidAttemptCount { .. } + | Self::ConnectionGenerationExhausted => None, } } } From fc61df54cf5709290d78bcecde1defc972238075 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 5 Sep 2026 15:32:42 +0900 Subject: [PATCH 031/105] fix(network): mint non-forgeable BiDi connection generations --- .../src/webdriver_bidi_connection.rs | 43 ++++++++++++++++--- 1 file changed, 36 insertions(+), 7 deletions(-) diff --git a/crates/originweave-network/src/webdriver_bidi_connection.rs b/crates/originweave-network/src/webdriver_bidi_connection.rs index 5d39bb5e3..ee350d141 100644 --- a/crates/originweave-network/src/webdriver_bidi_connection.rs +++ b/crates/originweave-network/src/webdriver_bidi_connection.rs @@ -1,6 +1,7 @@ use std::{ io, net::{SocketAddr, TcpStream}, + sync::atomic::{AtomicU64, Ordering}, time::Duration, }; @@ -15,6 +16,18 @@ pub use error::WebDriverBiDiTcpConnectionError; #[cfg(test)] mod tests; +static NEXT_CONNECTION_GENERATION: AtomicU64 = AtomicU64::new(1); + +fn allocate_connection_generation( + counter: &AtomicU64, +) -> Result { + counter + .fetch_update(Ordering::Relaxed, Ordering::Relaxed, |current| { + current.checked_add(1) + }) + .map_err(|_| WebDriverBiDiTcpConnectionError::ConnectionGenerationExhausted) +} + fn is_retryable_connect_error(kind: io::ErrorKind) -> bool { matches!( kind, @@ -32,7 +45,9 @@ fn is_retryable_connect_error(kind: io::ErrorKind) -> bool { /// produced by `originweave-core`. It applies the same bounded per-attempt timeout and retry /// ceilings as the general direct-network connector, opens only the exact [`SocketAddr`] carried by /// that target, and does not expose the stream until the operating system's observed peer has been -/// verified by the consumed target. +/// verified by the consumed target. Each verified stream also receives one process-local monotonic +/// connection generation that later transport stages can retain as non-forgeable correlation +/// provenance; the generation is not public authority and is never accepted from callers. /// /// This boundary performs no DNS lookup, proxy or PAC routing, Chromium/ChromeDriver process /// authentication, TLS negotiation, WebSocket upgrade, BiDi framing, browser policy decision, or @@ -107,11 +122,14 @@ impl WebDriverBiDiTcpConnectionPlan { attempt_number, source, })?; + let connection_generation = + allocate_connection_generation(&NEXT_CONNECTION_GENERATION)?; return Ok(WebDriverBiDiTcpConnection { stream, verified_peer, attempt_number, connect_timeout, + connection_generation, }); } Err(source) @@ -170,13 +188,16 @@ impl WebDriverBiDiSocketConnector for SystemWebDriverBiDiConnector { /// /// This wrapper proves only exact transport-destination equality for one bounded connection. The /// caller must still establish any required TLS channel, complete a WebSocket handshake, bind the -/// transport to the expected browser process/session, and pass separate action-policy checks. +/// transport to the expected browser process/session, and pass separate action-policy checks. A +/// private process-local connection generation follows this exact stream so later evidence cannot +/// be mixed with another connection that happens to use the same session or command identifier. #[derive(Debug)] pub struct WebDriverBiDiTcpConnection { stream: TcpStream, verified_peer: VerifiedWebDriverBiDiSocketPeer, attempt_number: u8, connect_timeout: Duration, + connection_generation: u64, } impl WebDriverBiDiTcpConnection { @@ -207,15 +228,17 @@ impl WebDriverBiDiTcpConnection { /// Consume the wrapper into the original verified stream and credential-free transport evidence. /// /// This handoff does not clone the socket or create reusable connection authority. The returned - /// evidence records only the already-verified peer plus bounded connection-attempt metadata; it - /// does not authenticate a browser process, establish TLS, complete WebSocket framing, or grant - /// browser or Agent authority. + /// evidence records the already-verified peer, bounded connection-attempt metadata, and one + /// private process-local connection generation for downstream provenance matching. It does not + /// authenticate a browser process, establish TLS, complete WebSocket framing, or grant browser + /// or Agent authority. #[must_use] pub fn into_parts(self) -> (TcpStream, WebDriverBiDiTcpConnectionEvidence) { let evidence = WebDriverBiDiTcpConnectionEvidence { verified_peer: self.verified_peer, attempt_number: self.attempt_number, connect_timeout: self.connect_timeout, + connection_generation: self.connection_generation, }; (self.stream, evidence) } @@ -224,13 +247,15 @@ impl WebDriverBiDiTcpConnection { /// Credential-free evidence retained when a verified WebDriver BiDi TCP stream is consumed. /// /// This value records exact peer/session/TLS-requirement metadata inherited from the consumed -/// no-DNS target together with the successful bounded attempt and per-attempt timeout. It is -/// transport evidence only and grants no process, TLS, WebSocket, browser-action, or Agent authority. +/// no-DNS target together with the successful bounded attempt, per-attempt timeout, and a private +/// process-local connection generation. It is transport evidence only and grants no process, TLS, +/// WebSocket, browser-action, or Agent authority. #[derive(Debug)] pub struct WebDriverBiDiTcpConnectionEvidence { verified_peer: VerifiedWebDriverBiDiSocketPeer, attempt_number: u8, connect_timeout: Duration, + connection_generation: u64, } impl WebDriverBiDiTcpConnectionEvidence { @@ -251,4 +276,8 @@ impl WebDriverBiDiTcpConnectionEvidence { pub const fn connect_timeout(&self) -> Duration { self.connect_timeout } + + pub(crate) const fn connection_generation(&self) -> u64 { + self.connection_generation + } } From c7ce08af3b01aea5efab802fb2217f35719cb9ba Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 5 Sep 2026 15:33:16 +0900 Subject: [PATCH 032/105] test(network): cover BiDi connection-generation allocator --- .../src/webdriver_bidi_connection/tests.rs | 36 ++++++++++++++----- 1 file changed, 27 insertions(+), 9 deletions(-) diff --git a/crates/originweave-network/src/webdriver_bidi_connection/tests.rs b/crates/originweave-network/src/webdriver_bidi_connection/tests.rs index e2d287ca1..54712fb5a 100644 --- a/crates/originweave-network/src/webdriver_bidi_connection/tests.rs +++ b/crates/originweave-network/src/webdriver_bidi_connection/tests.rs @@ -6,6 +6,7 @@ use std::{ error::Error, io, net::{SocketAddr, TcpListener, TcpStream}, + sync::atomic::AtomicU64, time::Duration, }; @@ -13,7 +14,7 @@ use originweave_core::{WebDriverBiDiWebSocketConnectTarget, WebDriverBiDiWebSock use super::{ WebDriverBiDiSocketConnector, WebDriverBiDiTcpConnectionError, WebDriverBiDiTcpConnectionPlan, - is_retryable_connect_error, + allocate_connection_generation, is_retryable_connect_error, }; use crate::connection::{MAX_CONNECT_TIMEOUT, MAX_CONNECTION_ATTEMPTS}; @@ -115,6 +116,19 @@ fn plan(maximum_attempts: u8) -> WebDriverBiDiTcpConnectionPlan { .expect("valid test plan") } +#[test] +fn connection_generation_allocator_is_monotonic_and_fails_before_reuse() { + let counter = AtomicU64::new(41); + assert_eq!(allocate_connection_generation(&counter).ok(), Some(41)); + assert_eq!(allocate_connection_generation(&counter).ok(), Some(42)); + + let exhausted = AtomicU64::new(u64::MAX); + assert!(matches!( + allocate_connection_generation(&exhausted), + Err(WebDriverBiDiTcpConnectionError::ConnectionGenerationExhausted) + )); +} + #[test] fn validates_timeout_and_attempt_bounds_before_io() { let zero_timeout = @@ -316,6 +330,7 @@ fn error_display_source_and_attempt_contracts_cover_every_variant() { attempt_count: 0, maximum_attempts: MAX_CONNECTION_ATTEMPTS, }, + WebDriverBiDiTcpConnectionError::ConnectionGenerationExhausted, WebDriverBiDiTcpConnectionError::ConnectionTimedOut { socket_address: socket_address(), attempt_count: 2, @@ -341,21 +356,24 @@ fn error_display_source_and_attempt_contracts_cover_every_variant() { let messages: Vec = errors.iter().map(ToString::to_string).collect(); assert!(messages[0].contains("outside 1ns")); assert!(messages[1].contains("attempt count 0")); - assert!(messages[2].contains("timed out after 2 attempts")); - assert!(messages[3].contains("failed after 3 attempts")); - assert!(messages[4].contains("peer inspection failed")); - assert!(messages[5].contains("did not match the approved target")); + assert!(messages[2].contains("generation space is exhausted")); + assert!(messages[3].contains("timed out after 2 attempts")); + assert!(messages[4].contains("failed after 3 attempts")); + assert!(messages[5].contains("peer inspection failed")); + assert!(messages[6].contains("did not match the approved target")); assert_eq!(errors[0].attempt_count(), None); assert_eq!(errors[1].attempt_count(), None); - assert_eq!(errors[2].attempt_count(), Some(2)); - assert_eq!(errors[3].attempt_count(), Some(3)); - assert_eq!(errors[4].attempt_count(), Some(1)); + assert_eq!(errors[2].attempt_count(), None); + assert_eq!(errors[3].attempt_count(), Some(2)); + assert_eq!(errors[4].attempt_count(), Some(3)); assert_eq!(errors[5].attempt_count(), Some(1)); + assert_eq!(errors[6].attempt_count(), Some(1)); assert!(errors[0].source().is_none()); assert!(errors[1].source().is_none()); - assert!(errors[2].source().is_some()); + assert!(errors[2].source().is_none()); assert!(errors[3].source().is_some()); assert!(errors[4].source().is_some()); assert!(errors[5].source().is_some()); + assert!(errors[6].source().is_some()); } From ef62a4ec8de3a5a6baba85d7071de8db098d28b6 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 5 Sep 2026 15:33:56 +0900 Subject: [PATCH 033/105] fix(network): retain connection provenance in BiDi correlation --- .../src/webdriver_bidi_command_correlation.rs | 72 +++++++++++++++---- 1 file changed, 58 insertions(+), 14 deletions(-) diff --git a/crates/originweave-network/src/webdriver_bidi_command_correlation.rs b/crates/originweave-network/src/webdriver_bidi_command_correlation.rs index 43d5e911e..a94cb09ba 100644 --- a/crates/originweave-network/src/webdriver_bidi_command_correlation.rs +++ b/crates/originweave-network/src/webdriver_bidi_command_correlation.rs @@ -25,6 +25,12 @@ pub enum WebDriverBiDiCommandKind { SessionEnd, } +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +struct OutstandingCommand { + kind: WebDriverBiDiCommandKind, + connection_generation: Option, +} + /// Outcome of a response after it has consumed the matching outstanding command identifier. #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub enum WebDriverBiDiCorrelatedResponseOutcome { @@ -36,12 +42,15 @@ pub enum WebDriverBiDiCorrelatedResponseOutcome { /// Credential-free evidence that one parsed response consumed one outstanding local command. /// -/// This value carries only the matched command identifier and success/error classification. It -/// does not retain result bodies, error text, browser authority, transport authority, or secrets. +/// This value carries only the matched command identifier and success/error classification. A +/// private process-local connection generation is retained when the command owner bound one before +/// I/O so later transport evidence can be compared without accepting caller-supplied provenance. +/// It does not retain result bodies, error text, browser authority, transport authority, or secrets. #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub struct WebDriverBiDiCorrelatedResponse { command_id: u64, outcome: WebDriverBiDiCorrelatedResponseOutcome, + connection_generation: Option, } impl WebDriverBiDiCorrelatedResponse { @@ -56,6 +65,10 @@ impl WebDriverBiDiCorrelatedResponse { pub const fn outcome(&self) -> WebDriverBiDiCorrelatedResponseOutcome { self.outcome } + + pub(crate) const fn connection_generation(&self) -> Option { + self.connection_generation + } } /// Fail-closed command-correlation failures at the local WebDriver BiDi response boundary. @@ -106,14 +119,16 @@ impl Error for WebDriverBiDiCommandCorrelationError {} /// Bounded local WebDriver BiDi command-response correlation state. /// /// Register an id together with its exact typed command family only after the caller has committed -/// to that outbound command. A success or correlatable error response consumes the id exactly once -/// only through a matching typed consumer. Events, null-id errors, and command-kind mismatches leave -/// outstanding state untouched. This type performs no I/O, retry, command serialization, browser -/// authentication, or authority grant. Debug output reports only the outstanding-count summary; -/// command identifiers and command families remain private correlation state. +/// to that outbound command. Connection-owning command adapters may additionally bind the private +/// generation of the exact established transport before I/O. A success or correlatable error +/// response consumes the id exactly once only through a matching typed consumer. Events, null-id +/// errors, and command-kind mismatches leave outstanding state untouched. This type performs no I/O, +/// retry, command serialization, browser authentication, or authority grant. Debug output reports +/// only the outstanding-count summary; command identifiers, families, and generations remain +/// private correlation state. #[derive(Default)] pub struct WebDriverBiDiCommandCorrelation { - outstanding: BTreeMap, + outstanding: BTreeMap, } impl fmt::Debug for WebDriverBiDiCommandCorrelation { @@ -147,6 +162,28 @@ impl WebDriverBiDiCommandCorrelation { &mut self, command_id: u64, command_kind: WebDriverBiDiCommandKind, + ) -> Result<(), WebDriverBiDiCommandCorrelationError> { + self.register(command_id, command_kind, None) + } + + pub(crate) fn register_command_for_connection( + &mut self, + command_id: u64, + command_kind: WebDriverBiDiCommandKind, + connection_generation: u64, + ) -> Result<(), WebDriverBiDiCommandCorrelationError> { + self.register( + command_id, + command_kind, + Some(connection_generation), + ) + } + + fn register( + &mut self, + command_id: u64, + command_kind: WebDriverBiDiCommandKind, + connection_generation: Option, ) -> Result<(), WebDriverBiDiCommandCorrelationError> { if command_id > MAX_WEBDRIVER_BIDI_JS_UINT { return Err(WebDriverBiDiCommandCorrelationError::CommandIdOutOfRange); @@ -157,7 +194,13 @@ impl WebDriverBiDiCommandCorrelation { if self.outstanding.len() >= MAX_WEBDRIVER_BIDI_OUTSTANDING_COMMANDS { return Err(WebDriverBiDiCommandCorrelationError::OutstandingCommandLimit); } - let _previous = self.outstanding.insert(command_id, command_kind); + let _previous = self.outstanding.insert( + command_id, + OutstandingCommand { + kind: command_kind, + connection_generation, + }, + ); Ok(()) } @@ -211,19 +254,19 @@ impl WebDriverBiDiCommandCorrelation { &self, command_id: u64, expected_kind: WebDriverBiDiCommandKind, - ) -> Result<(), WebDriverBiDiCommandCorrelationError> { + ) -> Result { let actual = self .outstanding .get(&command_id) .copied() .ok_or(WebDriverBiDiCommandCorrelationError::CommandNotOutstanding)?; - if actual != expected_kind { + if actual.kind != expected_kind { return Err(WebDriverBiDiCommandCorrelationError::CommandKindMismatch { expected: expected_kind, - actual, + actual: actual.kind, }); } - Ok(()) + Ok(actual) } fn complete( @@ -232,11 +275,12 @@ impl WebDriverBiDiCommandCorrelation { expected_kind: WebDriverBiDiCommandKind, outcome: WebDriverBiDiCorrelatedResponseOutcome, ) -> Result { - self.require_command_kind(command_id, expected_kind)?; + let outstanding = self.require_command_kind(command_id, expected_kind)?; let _removed = self.outstanding.remove(&command_id); Ok(WebDriverBiDiCorrelatedResponse { command_id, outcome, + connection_generation: outstanding.connection_generation, }) } } From 04f066c55c25a16973c9cb66b893560d4df90cda Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 5 Sep 2026 15:34:20 +0900 Subject: [PATCH 034/105] fix(network): bind session.end correlation to exact connection --- .../src/webdriver_bidi_session_end_command.rs | 20 +++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/crates/originweave-network/src/webdriver_bidi_session_end_command.rs b/crates/originweave-network/src/webdriver_bidi_session_end_command.rs index 29fbe05aa..d8a07b0ed 100644 --- a/crates/originweave-network/src/webdriver_bidi_session_end_command.rs +++ b/crates/originweave-network/src/webdriver_bidi_session_end_command.rs @@ -41,11 +41,12 @@ impl WebDriverBiDiSessionEndCommand { /// Register and write this exact command on an already established verified BiDi stream. /// /// Locally invalid frame deadlines fail before correlation registration and before any remote - /// side effect. Correlation then registers the command before the first possible frame write. - /// A frame-owner preflight rejection that proves no write began retires this exact command - /// again. Once frame emission can have begun, a later failure leaves the identifier outstanding - /// because partial or full emission is ambiguous. A successful write also leaves the identifier - /// outstanding until a later correlated response proves completion. + /// side effect. Correlation then binds the command id and command family to the private + /// process-local generation of this exact established connection before the first possible + /// frame write. A frame-owner preflight rejection that proves no write began retires this exact + /// command again. Once frame emission can have begun, a later failure leaves the identifier + /// outstanding because partial or full emission is ambiguous. A successful write also leaves + /// the identifier outstanding until a later correlated response proves completion. pub fn send( self, established: WebDriverBiDiWebSocketEstablished, @@ -61,8 +62,15 @@ impl WebDriverBiDiSessionEndCommand { }, }); } + let connection_generation = established + .transport_evidence() + .connection_generation(); correlation - .register_command_for(self.command_id, WebDriverBiDiCommandKind::SessionEnd) + .register_command_for_connection( + self.command_id, + WebDriverBiDiCommandKind::SessionEnd, + connection_generation, + ) .map_err(|source| WebDriverBiDiSessionEndCommandError::Correlation { source })?; let message = self.serialized(); match established.write_text_frame(&message, masking_key, frame_timeout) { From 54265a231f4b8f13f65f2bf802145834467e1a0a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 5 Sep 2026 15:34:48 +0900 Subject: [PATCH 035/105] fix(network): bind session.end acknowledgments to connection provenance --- .../webdriver_bidi_session_end_response.rs | 55 +++++++++++++++---- 1 file changed, 44 insertions(+), 11 deletions(-) diff --git a/crates/originweave-network/src/webdriver_bidi_session_end_response.rs b/crates/originweave-network/src/webdriver_bidi_session_end_response.rs index 9ab083230..594c2d947 100644 --- a/crates/originweave-network/src/webdriver_bidi_session_end_response.rs +++ b/crates/originweave-network/src/webdriver_bidi_session_end_response.rs @@ -11,22 +11,27 @@ use crate::{ /// WebDriver BiDi defines `session.EndResult` as the extensible `EmptyResult` object. The common /// local-end envelope parser already validates the complete JSON document and requires a success /// `result` object, so this command-specific boundary intentionally retains no generic result body -/// and accepts extension members. This value proves only that the remote end returned a correlated -/// protocol success; it does not prove Chromium process exit, profile deletion, resource release, -/// or any other OriginWeave operational teardown postcondition. +/// and accepts extension members. The result retains the private process-local generation of the +/// exact connection on which the command was registered before I/O so later teardown evidence can +/// reject another connection even when both reuse the same WebDriver session and command id. This +/// value proves only that the remote end returned a correlated protocol success; it does not prove +/// Chromium process exit, profile deletion, resource release, or any other OriginWeave operational +/// teardown postcondition. #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub struct WebDriverBiDiSessionEndResult { command_id: u64, + connection_generation: u64, } impl WebDriverBiDiSessionEndResult { /// Parse one bounded local-end message and consume its exact outstanding command on response. /// /// Complete JSON and common WebDriver BiDi envelope validation occur before correlation state - /// can be consumed. Successful responses retain only the matched command id. A correlatable - /// protocol-error response consumes its matching id and returns a typed remote failure, while - /// events, null-id errors, malformed envelopes, unknown ids, and command-kind mismatches fail - /// closed without consuming unrelated outstanding state. + /// can be consumed. Successful responses require the connection generation registered by the + /// typed `session.end` sender and retain only that private provenance plus the matched command + /// id. A correlatable protocol-error response consumes its matching id and returns a typed remote + /// failure, while events, null-id errors, malformed envelopes, unknown ids, and command-kind + /// mismatches fail closed without consuming unrelated outstanding state. pub fn parse_and_correlate( message: &WebDriverBiDiWebSocketTextMessage, correlation: &mut WebDriverBiDiCommandCorrelation, @@ -38,9 +43,17 @@ impl WebDriverBiDiSessionEndResult { .map_err(|source| WebDriverBiDiSessionEndResponseError::Correlation { source })?; match completed.outcome() { - WebDriverBiDiCorrelatedResponseOutcome::Success => Ok(Self { - command_id: completed.command_id(), - }), + WebDriverBiDiCorrelatedResponseOutcome::Success => { + let connection_generation = completed.connection_generation().ok_or( + WebDriverBiDiSessionEndResponseError::MissingConnectionProvenance { + command_id: completed.command_id(), + }, + )?; + Ok(Self { + command_id: completed.command_id(), + connection_generation, + }) + } WebDriverBiDiCorrelatedResponseOutcome::Error => { Err(WebDriverBiDiSessionEndResponseError::RemoteProtocolError { command_id: completed.command_id(), @@ -54,6 +67,10 @@ impl WebDriverBiDiSessionEndResult { pub const fn command_id(&self) -> u64 { self.command_id } + + pub(crate) const fn connection_generation(&self) -> u64 { + self.connection_generation + } } /// Fail-closed failures while admitting one typed WebDriver BiDi `session.end` response. @@ -69,6 +86,11 @@ pub enum WebDriverBiDiSessionEndResponseError { /// Exact typed correlation failure. source: WebDriverBiDiCommandCorrelationError, }, + /// A successful response consumed a `session.end` correlation that lacked transport provenance. + MissingConnectionProvenance { + /// Exact local command identifier whose registration omitted the connection generation. + command_id: u64, + }, /// The remote end returned a correlatable WebDriver BiDi protocol error for this command. RemoteProtocolError { /// Exact local command identifier consumed by the protocol-error response. @@ -85,6 +107,8 @@ impl fmt::Display for WebDriverBiDiSessionEndResponseError { Self::Correlation { .. } => { formatter.write_str("WebDriver BiDi session.end response correlation failed") } + Self::MissingConnectionProvenance { .. } => formatter + .write_str("WebDriver BiDi session.end response lacks connection provenance"), Self::RemoteProtocolError { .. } => { formatter.write_str("WebDriver BiDi session.end returned a protocol error") } @@ -97,7 +121,7 @@ impl Error for WebDriverBiDiSessionEndResponseError { match self { Self::Envelope { source } => Some(source), Self::Correlation { source } => Some(source), - Self::RemoteProtocolError { .. } => None, + Self::MissingConnectionProvenance { .. } | Self::RemoteProtocolError { .. } => None, } } } @@ -126,6 +150,15 @@ mod tests { ); assert!(correlation.source().is_some()); + let missing = WebDriverBiDiSessionEndResponseError::MissingConnectionProvenance { + command_id: 7, + }; + assert_eq!( + missing.to_string(), + "WebDriver BiDi session.end response lacks connection provenance" + ); + assert!(missing.source().is_none()); + let remote = WebDriverBiDiSessionEndResponseError::RemoteProtocolError { command_id: 7 }; assert_eq!( remote.to_string(), From 8a1b9eefaa3883fff860d28f3c9db690fc459d5c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 5 Sep 2026 15:35:17 +0900 Subject: [PATCH 036/105] test(network): require session.end transport provenance --- .../webdriver_bidi_session_end_response.rs | 25 ++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/crates/originweave-network/tests/webdriver_bidi_session_end_response.rs b/crates/originweave-network/tests/webdriver_bidi_session_end_response.rs index 042f5137b..f35d012e6 100644 --- a/crates/originweave-network/tests/webdriver_bidi_session_end_response.rs +++ b/crates/originweave-network/tests/webdriver_bidi_session_end_response.rs @@ -8,7 +8,7 @@ use std::{ use originweave_core::WebDriverBiDiWebSocketEndpoint; use originweave_network::{ - WebDriverBiDiCommandCorrelation, WebDriverBiDiSessionEndCommand, + WebDriverBiDiCommandCorrelation, WebDriverBiDiCommandKind, WebDriverBiDiSessionEndCommand, WebDriverBiDiSessionEndResponseError, WebDriverBiDiSessionEndResult, WebDriverBiDiTcpConnectionPlan, WebDriverBiDiWebSocketClientKey, WebDriverBiDiWebSocketHandshakePlan, WebDriverBiDiWebSocketMaskKey, @@ -145,6 +145,29 @@ fn session_end_success_accepts_extensible_empty_result_and_consumes_exact_correl Ok(()) } +#[test] +fn session_end_success_rejects_correlation_without_connection_provenance() +-> Result<(), Box> { + let (text, _connection_bound_correlation) = send_end_and_read_response(END_SUCCESS_RESPONSE)?; + let mut unbound = WebDriverBiDiCommandCorrelation::new(); + unbound.register_command_for(7, WebDriverBiDiCommandKind::SessionEnd)?; + + let parsed = WebDriverBiDiSessionEndResult::parse_and_correlate(&text, &mut unbound); + let error = parsed + .err() + .ok_or_else(|| io::Error::other("unbound session.end correlation was accepted"))?; + assert!(matches!( + error, + WebDriverBiDiSessionEndResponseError::MissingConnectionProvenance { command_id: 7 } + )); + assert_eq!( + error.to_string(), + "WebDriver BiDi session.end response lacks connection provenance" + ); + assert_eq!(unbound.outstanding_count(), 0); + Ok(()) +} + #[test] fn session_end_remote_error_consumes_only_the_correlated_command() -> Result<(), Box> { let (text, mut correlation) = send_end_and_read_response(END_REMOTE_ERROR_RESPONSE)?; From dddb93c6af673d948fca522110765b86a56c84cf Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 5 Sep 2026 15:35:39 +0900 Subject: [PATCH 037/105] fix(network): retain exact connection on transport closure evidence --- ...driver_bidi_websocket_transport_closure.rs | 32 ++++++++++++++----- 1 file changed, 24 insertions(+), 8 deletions(-) diff --git a/crates/originweave-network/src/webdriver_bidi_websocket_transport_closure.rs b/crates/originweave-network/src/webdriver_bidi_websocket_transport_closure.rs index bea19f905..0f5be7b49 100644 --- a/crates/originweave-network/src/webdriver_bidi_websocket_transport_closure.rs +++ b/crates/originweave-network/src/webdriver_bidi_websocket_transport_closure.rs @@ -18,14 +18,17 @@ pub enum WebDriverBiDiWebSocketTransportClosureKind { /// Credential-free observation that one established WebDriver BiDi transport ceased carrying data. /// /// Construction consumes the established WebSocket, so this value cannot be used to regain the -/// underlying connection. It records only a validated peer Close status when one was actually -/// present, or clean TCP EOF before a new frame began. It grants no browser, process, profile, -/// policy, secret, retry, reconnect, or Agent authority and does not perform a reciprocal Close -/// handshake. +/// underlying connection. It retains the private process-local generation of that exact connection +/// so a later teardown consumer can reject closure observed on another socket even when session and +/// command identifiers are reused. It records only a validated peer Close status when one was +/// actually present, or clean TCP EOF before a new frame began. It grants no browser, process, +/// profile, policy, secret, retry, reconnect, or Agent authority and does not perform a reciprocal +/// Close handshake. #[derive(Debug, Eq, PartialEq)] pub struct WebDriverBiDiWebSocketTransportClosureObservation { kind: WebDriverBiDiWebSocketTransportClosureKind, peer_close_status_code: Option, + connection_generation: u64, } impl WebDriverBiDiWebSocketTransportClosureObservation { @@ -41,18 +44,25 @@ impl WebDriverBiDiWebSocketTransportClosureObservation { established: WebDriverBiDiWebSocketEstablished, frame_timeout: Duration, ) -> Result { - Self::observe_frame(established, frame_timeout, true) + let connection_generation = established + .transport_evidence() + .connection_generation(); + Self::observe_frame(established, frame_timeout, true, connection_generation) } fn observe_frame( established: WebDriverBiDiWebSocketEstablished, frame_timeout: Duration, allow_pong: bool, + connection_generation: u64, ) -> Result { match established.read_frame(frame_timeout) { - Ok((established, frame)) if frame.opcode() == 0xa && allow_pong => { - Self::observe_frame(established, frame_timeout, false) - } + Ok((established, frame)) if frame.opcode() == 0xa && allow_pong => Self::observe_frame( + established, + frame_timeout, + false, + connection_generation, + ), Ok((established, frame)) => { if frame.opcode() != 0x8 { return Err( @@ -69,11 +79,13 @@ impl WebDriverBiDiWebSocketTransportClosureObservation { Ok(Self { kind: WebDriverBiDiWebSocketTransportClosureKind::PeerCloseFrame, peer_close_status_code, + connection_generation, }) } Err(WebDriverBiDiWebSocketFrameError::FrameEnded { bytes_read: 0 }) => Ok(Self { kind: WebDriverBiDiWebSocketTransportClosureKind::PeerEof, peer_close_status_code: None, + connection_generation, }), Err(source) => Err(WebDriverBiDiWebSocketTransportClosureError::Frame { source }), } @@ -90,6 +102,10 @@ impl WebDriverBiDiWebSocketTransportClosureObservation { pub const fn peer_close_status_code(&self) -> Option { self.peer_close_status_code } + + pub(crate) const fn connection_generation(&self) -> u64 { + self.connection_generation + } } /// Fail-closed errors while converting one established BiDi transport into closure evidence. From 9452fc37d0197c6b01abc7d25df4f2ffab334a23 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 5 Sep 2026 15:36:04 +0900 Subject: [PATCH 038/105] fix(network): reject teardown evidence from another connection --- .../src/webdriver_bidi_session_teardown.rs | 53 +++++++++++++++---- 1 file changed, 44 insertions(+), 9 deletions(-) diff --git a/crates/originweave-network/src/webdriver_bidi_session_teardown.rs b/crates/originweave-network/src/webdriver_bidi_session_teardown.rs index 166eaa09b..7d1295677 100644 --- a/crates/originweave-network/src/webdriver_bidi_session_teardown.rs +++ b/crates/originweave-network/src/webdriver_bidi_session_teardown.rs @@ -1,3 +1,5 @@ +use std::{error::Error, fmt}; + use crate::{WebDriverBiDiSessionEndResult, WebDriverBiDiWebSocketTransportClosureObservation}; /// Fail-closed operational disposition derived from teardown observations. @@ -39,7 +41,10 @@ impl WebDriverBiDiSessionTeardownObservations { } } - /// Return whether typed closure evidence for the exact session transport was supplied. + /// Return whether typed closure evidence for the exact acknowledged transport was supplied. + /// + /// A teardown assessment is constructed only after connection-generation equality has been + /// checked, so a retained observation belongs to the same exact connection as its protocol ack. #[must_use] pub const fn transport_closed_observed(&self) -> bool { self.transport_closure_observation.is_some() @@ -54,12 +59,32 @@ impl WebDriverBiDiSessionTeardownObservations { } } +/// Fail-closed failures while binding protocol acknowledgment to operational teardown evidence. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum WebDriverBiDiSessionTeardownAssessmentError { + /// The closure observation belongs to a different process-local connection generation. + TransportConnectionMismatch, +} + +impl fmt::Display for WebDriverBiDiSessionTeardownAssessmentError { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::TransportConnectionMismatch => formatter.write_str( + "WebDriver BiDi transport closure does not match the acknowledged connection", + ), + } + } +} + +impl Error for WebDriverBiDiSessionTeardownAssessmentError {} + /// One correlated `session.end` acknowledgment kept separate from operational teardown evidence. /// /// A protocol acknowledgment alone is never operational completion. Typed transport closure is -/// retained from the consumed WebSocket, while process/profile evidence remains absent until its -/// owning runtime boundaries can provide typed observations. Consequently this assessment remains -/// fail closed in the current dependency-ordered slice. +/// retained only when its private process-local connection generation matches the generation bound +/// to the acknowledged `session.end` command before I/O. Process/profile evidence remains absent +/// until its owning runtime boundaries can provide typed observations. Consequently this assessment +/// remains fail closed in the current dependency-ordered slice. #[derive(Debug, Eq, PartialEq)] pub struct WebDriverBiDiSessionTeardownAssessment { protocol_ack: WebDriverBiDiSessionEndResult, @@ -68,15 +93,25 @@ pub struct WebDriverBiDiSessionTeardownAssessment { impl WebDriverBiDiSessionTeardownAssessment { /// Bind one correlated protocol acknowledgment to separately supplied operational observations. - #[must_use] - pub const fn from_protocol_ack( + /// + /// If typed transport closure is present, its private connection generation must equal the one + /// retained by the protocol acknowledgment. A closure from another socket is rejected even when + /// both transports reuse the same WebDriver session identifier and command id. + pub fn from_protocol_ack( protocol_ack: WebDriverBiDiSessionEndResult, observations: WebDriverBiDiSessionTeardownObservations, - ) -> Self { - Self { + ) -> Result { + if let Some(transport_closure) = observations.transport_closure_observation() { + if transport_closure.connection_generation() != protocol_ack.connection_generation() { + return Err( + WebDriverBiDiSessionTeardownAssessmentError::TransportConnectionMismatch, + ); + } + } + Ok(Self { protocol_ack, observations, - } + }) } /// Return the exact command id proven by the correlated protocol acknowledgment. From 77b294ab85fb1806f004d6659b9bf00c3ac584ae Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 5 Sep 2026 15:36:32 +0900 Subject: [PATCH 039/105] test(network): verify exact transport generation at teardown --- ...driver_bidi_session_teardown_assessment.rs | 35 ++++++++++++------- 1 file changed, 22 insertions(+), 13 deletions(-) diff --git a/crates/originweave-network/tests/webdriver_bidi_session_teardown_assessment.rs b/crates/originweave-network/tests/webdriver_bidi_session_teardown_assessment.rs index 5a38817ad..dc4f191a1 100644 --- a/crates/originweave-network/tests/webdriver_bidi_session_teardown_assessment.rs +++ b/crates/originweave-network/tests/webdriver_bidi_session_teardown_assessment.rs @@ -9,12 +9,13 @@ use std::{ use originweave_core::WebDriverBiDiWebSocketEndpoint; use originweave_network::{ WebDriverBiDiCommandCorrelation, WebDriverBiDiSessionEndCommand, WebDriverBiDiSessionEndResult, - WebDriverBiDiSessionTeardownAssessment, WebDriverBiDiSessionTeardownDisposition, - WebDriverBiDiSessionTeardownObservations, WebDriverBiDiTcpConnectionPlan, - WebDriverBiDiWebSocketClientKey, WebDriverBiDiWebSocketEstablished, - WebDriverBiDiWebSocketHandshakePlan, WebDriverBiDiWebSocketMaskKey, - WebDriverBiDiWebSocketMessageAssembler, WebDriverBiDiWebSocketMessageAssembly, - WebDriverBiDiWebSocketTransportClosureKind, WebDriverBiDiWebSocketTransportClosureObservation, + WebDriverBiDiSessionTeardownAssessment, WebDriverBiDiSessionTeardownAssessmentError, + WebDriverBiDiSessionTeardownDisposition, WebDriverBiDiSessionTeardownObservations, + WebDriverBiDiTcpConnectionPlan, WebDriverBiDiWebSocketClientKey, + WebDriverBiDiWebSocketEstablished, WebDriverBiDiWebSocketHandshakePlan, + WebDriverBiDiWebSocketMaskKey, WebDriverBiDiWebSocketMessageAssembler, + WebDriverBiDiWebSocketMessageAssembly, WebDriverBiDiWebSocketTransportClosureKind, + WebDriverBiDiWebSocketTransportClosureObservation, }; const SESSION_ID: &str = "01234567-89ab-cdef-0123-456789abcdef"; @@ -155,7 +156,7 @@ fn missing_typed_transport_observation_keeps_teardown_pending() -> Result<(), Bo let assessment = WebDriverBiDiSessionTeardownAssessment::from_protocol_ack( acknowledged, WebDriverBiDiSessionTeardownObservations::new(transport_closure), - ); + )?; assert_eq!(assessment.command_id(), 7); assert_eq!( @@ -186,7 +187,7 @@ fn typed_transport_closure_cannot_complete_teardown_without_process_and_profile_ let assessment = WebDriverBiDiSessionTeardownAssessment::from_protocol_ack( acknowledged, WebDriverBiDiSessionTeardownObservations::new(Some(transport_closure)), - ); + )?; assert!(!assessment.is_operationally_complete()); assert_eq!( @@ -205,21 +206,29 @@ fn typed_transport_closure_cannot_complete_teardown_without_process_and_profile_ } #[test] -fn closure_from_another_connection_is_not_accepted_for_the_acknowledged_transport() +fn closure_from_another_connection_is_rejected_for_the_acknowledged_transport() -> Result<(), Box> { let (acknowledged_a, established_a) = correlated_session_end_ack_and_transport()?; let (_acknowledged_b, established_b) = correlated_session_end_ack_and_transport()?; drop(established_a); let closure_b = observed_transport_closure(established_b)?; - let assessment = WebDriverBiDiSessionTeardownAssessment::from_protocol_ack( + let result = WebDriverBiDiSessionTeardownAssessment::from_protocol_ack( acknowledged_a, WebDriverBiDiSessionTeardownObservations::new(Some(closure_b)), ); - assert!( - !assessment.observations().transport_closed_observed(), - "closure from a distinct WebSocket generation must not be attributed to the acknowledged session.end transport" + let error = result + .err() + .ok_or_else(|| io::Error::other("closure from another connection was accepted"))?; + assert_eq!( + error, + WebDriverBiDiSessionTeardownAssessmentError::TransportConnectionMismatch + ); + assert_eq!( + error.to_string(), + "WebDriver BiDi transport closure does not match the acknowledged connection" ); + assert!(error.source().is_none()); Ok(()) } From 23eea662e9b13569a1bd1ecbec34d1acc001642c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 5 Sep 2026 15:37:21 +0900 Subject: [PATCH 040/105] fix(network): expose teardown provenance mismatch error --- crates/originweave-network/src/lib.rs | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/crates/originweave-network/src/lib.rs b/crates/originweave-network/src/lib.rs index 1ff23ce4d..4c05319e4 100644 --- a/crates/originweave-network/src/lib.rs +++ b/crates/originweave-network/src/lib.rs @@ -9,10 +9,12 @@ //! unmasked server-frame reads, assembles bounded WebDriver BiDi text messages, //! classifies complete local-end JSON envelopes, tracks bounded command-response //! correlation, sends narrowly typed `session.status` and `session.end` commands, -//! admits typed correlated status and end responses, observes bounded peer Close -//! or clean-EOF transport cessation, and keeps protocol/transport evidence separate -//! from explicit operational teardown observations without exposing generic JSON -//! bodies or granting browser, TLS, policy, secret, process, profile, or Agent authority. +//! admits typed correlated status and end responses, binds `session.end` ACK and +//! closure evidence to one private process-local connection generation, observes +//! bounded peer Close or clean-EOF transport cessation, and keeps protocol/transport +//! evidence separate from explicit operational teardown observations without exposing +//! generic JSON bodies or granting browser, TLS, policy, secret, process, profile, +//! or Agent authority. #![forbid(unsafe_code)] #![deny(missing_docs)] @@ -67,8 +69,8 @@ pub use webdriver_bidi_session_status_response::{ WebDriverBiDiSessionStatusResult, }; pub use webdriver_bidi_session_teardown::{ - WebDriverBiDiSessionTeardownAssessment, WebDriverBiDiSessionTeardownDisposition, - WebDriverBiDiSessionTeardownObservations, + WebDriverBiDiSessionTeardownAssessment, WebDriverBiDiSessionTeardownAssessmentError, + WebDriverBiDiSessionTeardownDisposition, WebDriverBiDiSessionTeardownObservations, }; pub use webdriver_bidi_websocket_frame::{ MAX_WEBSOCKET_FRAME_PAYLOAD_SIZE, MAX_WEBSOCKET_FRAME_TIMEOUT, From 92392b2da33bcd5446c6d2eb1b5504c39e60e3d6 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 5 Sep 2026 15:40:43 +0900 Subject: [PATCH 041/105] test(network): cover same-session reconnect and cross-session teardown mixing --- ...driver_bidi_session_teardown_assessment.rs | 55 ++++++++++++++----- 1 file changed, 42 insertions(+), 13 deletions(-) diff --git a/crates/originweave-network/tests/webdriver_bidi_session_teardown_assessment.rs b/crates/originweave-network/tests/webdriver_bidi_session_teardown_assessment.rs index dc4f191a1..86b7f8c9e 100644 --- a/crates/originweave-network/tests/webdriver_bidi_session_teardown_assessment.rs +++ b/crates/originweave-network/tests/webdriver_bidi_session_teardown_assessment.rs @@ -19,6 +19,7 @@ use originweave_network::{ }; const SESSION_ID: &str = "01234567-89ab-cdef-0123-456789abcdef"; +const SECOND_SESSION_ID: &str = "fedcba98-7654-3210-fedc-ba9876543210"; const RFC6455_SAMPLE_KEY: &str = "dGhlIHNhbXBsZSBub25jZQ=="; const OPENING_RESPONSE: &[u8] = b"HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=\r\n\r\n"; const END_SUCCESS_RESPONSE: &[u8] = br#"{"type":"success","id":7,"result":{}}"#; @@ -67,7 +68,9 @@ fn read_masked_text_frame(stream: &mut TcpStream) -> io::Result> { Ok(payload) } -fn correlated_session_end_ack_and_transport() -> Result< +fn correlated_session_end_ack_and_transport_for( + session_id: &str, +) -> Result< ( WebDriverBiDiSessionEndResult, WebDriverBiDiWebSocketEstablished, @@ -92,9 +95,9 @@ fn correlated_session_end_ack_and_transport() -> Result< stream.write_all(NORMAL_CLOSE_FRAME) }); - let endpoint = format!("ws://{local_addr}/session/{SESSION_ID}"); + let endpoint = format!("ws://{local_addr}/session/{session_id}"); let target = WebDriverBiDiWebSocketEndpoint::new(&endpoint)? - .correlate_session_id(SESSION_ID)? + .correlate_session_id(session_id)? .into_explicit_connect_target()?; let connection = WebDriverBiDiTcpConnectionPlan::new(target, Duration::from_secs(1), 1)?.connect()?; @@ -128,6 +131,16 @@ fn correlated_session_end_ack_and_transport() -> Result< Ok((acknowledged, established)) } +fn correlated_session_end_ack_and_transport() -> Result< + ( + WebDriverBiDiSessionEndResult, + WebDriverBiDiWebSocketEstablished, + ), + Box, +> { + correlated_session_end_ack_and_transport_for(SESSION_ID) +} + fn observed_transport_closure( established: WebDriverBiDiWebSocketEstablished, ) -> Result> { @@ -205,17 +218,16 @@ fn typed_transport_closure_cannot_complete_teardown_without_process_and_profile_ Ok(()) } -#[test] -fn closure_from_another_connection_is_rejected_for_the_acknowledged_transport() --> Result<(), Box> { - let (acknowledged_a, established_a) = correlated_session_end_ack_and_transport()?; - let (_acknowledged_b, established_b) = correlated_session_end_ack_and_transport()?; - drop(established_a); - - let closure_b = observed_transport_closure(established_b)?; +fn assert_cross_connection_closure_rejected( + acknowledged: WebDriverBiDiSessionEndResult, + established_to_drop: WebDriverBiDiWebSocketEstablished, + foreign_established: WebDriverBiDiWebSocketEstablished, +) -> Result<(), Box> { + drop(established_to_drop); + let foreign_closure = observed_transport_closure(foreign_established)?; let result = WebDriverBiDiSessionTeardownAssessment::from_protocol_ack( - acknowledged_a, - WebDriverBiDiSessionTeardownObservations::new(Some(closure_b)), + acknowledged, + WebDriverBiDiSessionTeardownObservations::new(Some(foreign_closure)), ); let error = result @@ -232,3 +244,20 @@ fn closure_from_another_connection_is_rejected_for_the_acknowledged_transport() assert!(error.source().is_none()); Ok(()) } + +#[test] +fn reconnected_same_session_cannot_supply_closure_for_the_acknowledged_transport() +-> Result<(), Box> { + let (acknowledged_a, established_a) = correlated_session_end_ack_and_transport_for(SESSION_ID)?; + let (_acknowledged_b, established_b) = correlated_session_end_ack_and_transport_for(SESSION_ID)?; + assert_cross_connection_closure_rejected(acknowledged_a, established_a, established_b) +} + +#[test] +fn another_session_cannot_supply_closure_for_the_acknowledged_transport() +-> Result<(), Box> { + let (acknowledged_a, established_a) = correlated_session_end_ack_and_transport_for(SESSION_ID)?; + let (_acknowledged_b, established_b) = + correlated_session_end_ack_and_transport_for(SECOND_SESSION_ID)?; + assert_cross_connection_closure_rejected(acknowledged_a, established_a, established_b) +} From 4bb85c292f6372ba361ca482bb2d73971b0fa57c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 5 Sep 2026 15:43:42 +0900 Subject: [PATCH 042/105] test(network): prove same-endpoint reconnect provenance isolation --- ...driver_bidi_session_teardown_assessment.rs | 37 ++++++++++++++----- 1 file changed, 27 insertions(+), 10 deletions(-) diff --git a/crates/originweave-network/tests/webdriver_bidi_session_teardown_assessment.rs b/crates/originweave-network/tests/webdriver_bidi_session_teardown_assessment.rs index 86b7f8c9e..4a90805cf 100644 --- a/crates/originweave-network/tests/webdriver_bidi_session_teardown_assessment.rs +++ b/crates/originweave-network/tests/webdriver_bidi_session_teardown_assessment.rs @@ -68,7 +68,8 @@ fn read_masked_text_frame(stream: &mut TcpStream) -> io::Result> { Ok(payload) } -fn correlated_session_end_ack_and_transport_for( +fn correlated_session_end_ack_and_transport_on( + listener: &TcpListener, session_id: &str, ) -> Result< ( @@ -77,10 +78,10 @@ fn correlated_session_end_ack_and_transport_for( ), Box, > { - let listener = TcpListener::bind(("127.0.0.1", 0))?; let local_addr = listener.local_addr()?; + let server_listener = listener.try_clone()?; let server = thread::spawn(move || -> io::Result<()> { - let (mut stream, _) = listener.accept()?; + let (mut stream, _) = server_listener.accept()?; read_opening_request(&mut stream)?; stream.write_all(OPENING_RESPONSE)?; let command = read_masked_text_frame(&mut stream)?; @@ -131,6 +132,19 @@ fn correlated_session_end_ack_and_transport_for( Ok((acknowledged, established)) } +fn correlated_session_end_ack_and_transport_for( + session_id: &str, +) -> Result< + ( + WebDriverBiDiSessionEndResult, + WebDriverBiDiWebSocketEstablished, + ), + Box, +> { + let listener = TcpListener::bind(("127.0.0.1", 0))?; + correlated_session_end_ack_and_transport_on(&listener, session_id) +} + fn correlated_session_end_ack_and_transport() -> Result< ( WebDriverBiDiSessionEndResult, @@ -220,10 +234,8 @@ fn typed_transport_closure_cannot_complete_teardown_without_process_and_profile_ fn assert_cross_connection_closure_rejected( acknowledged: WebDriverBiDiSessionEndResult, - established_to_drop: WebDriverBiDiWebSocketEstablished, foreign_established: WebDriverBiDiWebSocketEstablished, ) -> Result<(), Box> { - drop(established_to_drop); let foreign_closure = observed_transport_closure(foreign_established)?; let result = WebDriverBiDiSessionTeardownAssessment::from_protocol_ack( acknowledged, @@ -246,18 +258,23 @@ fn assert_cross_connection_closure_rejected( } #[test] -fn reconnected_same_session_cannot_supply_closure_for_the_acknowledged_transport() +fn reconnected_same_session_and_endpoint_cannot_supply_closure_for_prior_transport() -> Result<(), Box> { - let (acknowledged_a, established_a) = correlated_session_end_ack_and_transport_for(SESSION_ID)?; - let (_acknowledged_b, established_b) = correlated_session_end_ack_and_transport_for(SESSION_ID)?; - assert_cross_connection_closure_rejected(acknowledged_a, established_a, established_b) + let listener = TcpListener::bind(("127.0.0.1", 0))?; + let (acknowledged_a, established_a) = + correlated_session_end_ack_and_transport_on(&listener, SESSION_ID)?; + drop(established_a); + let (_acknowledged_b, established_b) = + correlated_session_end_ack_and_transport_on(&listener, SESSION_ID)?; + assert_cross_connection_closure_rejected(acknowledged_a, established_b) } #[test] fn another_session_cannot_supply_closure_for_the_acknowledged_transport() -> Result<(), Box> { let (acknowledged_a, established_a) = correlated_session_end_ack_and_transport_for(SESSION_ID)?; + drop(established_a); let (_acknowledged_b, established_b) = correlated_session_end_ack_and_transport_for(SECOND_SESSION_ID)?; - assert_cross_connection_closure_rejected(acknowledged_a, established_a, established_b) + assert_cross_connection_closure_rejected(acknowledged_a, established_b) } From ae6495e5d73694ed7da241ef1ba03dd0522acc24 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 5 Sep 2026 15:45:56 +0900 Subject: [PATCH 043/105] refactor(network): type BiDi connection-generation provenance --- .../src/webdriver_bidi_connection.rs | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/crates/originweave-network/src/webdriver_bidi_connection.rs b/crates/originweave-network/src/webdriver_bidi_connection.rs index ee350d141..9a9be9843 100644 --- a/crates/originweave-network/src/webdriver_bidi_connection.rs +++ b/crates/originweave-network/src/webdriver_bidi_connection.rs @@ -18,13 +18,21 @@ mod tests; static NEXT_CONNECTION_GENERATION: AtomicU64 = AtomicU64::new(1); +/// Process-local identity of one verified WebDriver BiDi transport generation. +/// +/// The value is minted only by the connection owner, is never accepted from callers, and exists +/// solely to prevent evidence from distinct sockets being combined across later protocol stages. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub(crate) struct WebDriverBiDiConnectionGeneration(u64); + fn allocate_connection_generation( counter: &AtomicU64, -) -> Result { +) -> Result { counter .fetch_update(Ordering::Relaxed, Ordering::Relaxed, |current| { current.checked_add(1) }) + .map(WebDriverBiDiConnectionGeneration) .map_err(|_| WebDriverBiDiTcpConnectionError::ConnectionGenerationExhausted) } @@ -197,7 +205,7 @@ pub struct WebDriverBiDiTcpConnection { verified_peer: VerifiedWebDriverBiDiSocketPeer, attempt_number: u8, connect_timeout: Duration, - connection_generation: u64, + connection_generation: WebDriverBiDiConnectionGeneration, } impl WebDriverBiDiTcpConnection { @@ -255,7 +263,7 @@ pub struct WebDriverBiDiTcpConnectionEvidence { verified_peer: VerifiedWebDriverBiDiSocketPeer, attempt_number: u8, connect_timeout: Duration, - connection_generation: u64, + connection_generation: WebDriverBiDiConnectionGeneration, } impl WebDriverBiDiTcpConnectionEvidence { @@ -277,7 +285,7 @@ impl WebDriverBiDiTcpConnectionEvidence { self.connect_timeout } - pub(crate) const fn connection_generation(&self) -> u64 { + pub(crate) const fn connection_generation(&self) -> WebDriverBiDiConnectionGeneration { self.connection_generation } } From 9115fd880f56425337f6af41232838fd9cea3fd3 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 5 Sep 2026 15:46:52 +0900 Subject: [PATCH 044/105] test(network): type connection-generation allocator evidence --- .../src/webdriver_bidi_connection/tests.rs | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/crates/originweave-network/src/webdriver_bidi_connection/tests.rs b/crates/originweave-network/src/webdriver_bidi_connection/tests.rs index 54712fb5a..63564283f 100644 --- a/crates/originweave-network/src/webdriver_bidi_connection/tests.rs +++ b/crates/originweave-network/src/webdriver_bidi_connection/tests.rs @@ -13,7 +13,8 @@ use std::{ use originweave_core::{WebDriverBiDiWebSocketConnectTarget, WebDriverBiDiWebSocketEndpoint}; use super::{ - WebDriverBiDiSocketConnector, WebDriverBiDiTcpConnectionError, WebDriverBiDiTcpConnectionPlan, + WebDriverBiDiConnectionGeneration, WebDriverBiDiSocketConnector, + WebDriverBiDiTcpConnectionError, WebDriverBiDiTcpConnectionPlan, allocate_connection_generation, is_retryable_connect_error, }; use crate::connection::{MAX_CONNECT_TIMEOUT, MAX_CONNECTION_ATTEMPTS}; @@ -119,8 +120,14 @@ fn plan(maximum_attempts: u8) -> WebDriverBiDiTcpConnectionPlan { #[test] fn connection_generation_allocator_is_monotonic_and_fails_before_reuse() { let counter = AtomicU64::new(41); - assert_eq!(allocate_connection_generation(&counter).ok(), Some(41)); - assert_eq!(allocate_connection_generation(&counter).ok(), Some(42)); + assert_eq!( + allocate_connection_generation(&counter).ok(), + Some(WebDriverBiDiConnectionGeneration(41)) + ); + assert_eq!( + allocate_connection_generation(&counter).ok(), + Some(WebDriverBiDiConnectionGeneration(42)) + ); let exhausted = AtomicU64::new(u64::MAX); assert!(matches!( From ddc1082cc0eb50dd6663062267e827355db8f66a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 5 Sep 2026 15:49:31 +0900 Subject: [PATCH 045/105] refactor(network): carry typed BiDi generation through correlation --- .../src/webdriver_bidi_command_correlation.rs | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/crates/originweave-network/src/webdriver_bidi_command_correlation.rs b/crates/originweave-network/src/webdriver_bidi_command_correlation.rs index a94cb09ba..2bfdf6802 100644 --- a/crates/originweave-network/src/webdriver_bidi_command_correlation.rs +++ b/crates/originweave-network/src/webdriver_bidi_command_correlation.rs @@ -1,7 +1,8 @@ use std::{collections::BTreeMap, error::Error, fmt}; use crate::{ - MAX_WEBDRIVER_BIDI_JS_UINT, WebDriverBiDiJsonEnvelope, WebDriverBiDiJsonEnvelopeRouting, + webdriver_bidi_connection::WebDriverBiDiConnectionGeneration, MAX_WEBDRIVER_BIDI_JS_UINT, + WebDriverBiDiJsonEnvelope, WebDriverBiDiJsonEnvelopeRouting, }; /// Maximum number of local WebDriver BiDi commands retained as outstanding at once. @@ -28,7 +29,7 @@ pub enum WebDriverBiDiCommandKind { #[derive(Clone, Copy, Debug, Eq, PartialEq)] struct OutstandingCommand { kind: WebDriverBiDiCommandKind, - connection_generation: Option, + connection_generation: Option, } /// Outcome of a response after it has consumed the matching outstanding command identifier. @@ -50,7 +51,7 @@ pub enum WebDriverBiDiCorrelatedResponseOutcome { pub struct WebDriverBiDiCorrelatedResponse { command_id: u64, outcome: WebDriverBiDiCorrelatedResponseOutcome, - connection_generation: Option, + connection_generation: Option, } impl WebDriverBiDiCorrelatedResponse { @@ -66,7 +67,9 @@ impl WebDriverBiDiCorrelatedResponse { self.outcome } - pub(crate) const fn connection_generation(&self) -> Option { + pub(crate) const fn connection_generation( + &self, + ) -> Option { self.connection_generation } } @@ -170,7 +173,7 @@ impl WebDriverBiDiCommandCorrelation { &mut self, command_id: u64, command_kind: WebDriverBiDiCommandKind, - connection_generation: u64, + connection_generation: WebDriverBiDiConnectionGeneration, ) -> Result<(), WebDriverBiDiCommandCorrelationError> { self.register( command_id, @@ -183,7 +186,7 @@ impl WebDriverBiDiCommandCorrelation { &mut self, command_id: u64, command_kind: WebDriverBiDiCommandKind, - connection_generation: Option, + connection_generation: Option, ) -> Result<(), WebDriverBiDiCommandCorrelationError> { if command_id > MAX_WEBDRIVER_BIDI_JS_UINT { return Err(WebDriverBiDiCommandCorrelationError::CommandIdOutOfRange); From 2b7714313a439e54ce78297f4c6ae0be65b65295 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 5 Sep 2026 15:50:11 +0900 Subject: [PATCH 046/105] refactor(network): type session.end connection provenance --- .../src/webdriver_bidi_session_end_response.rs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/crates/originweave-network/src/webdriver_bidi_session_end_response.rs b/crates/originweave-network/src/webdriver_bidi_session_end_response.rs index 594c2d947..c1c9314d4 100644 --- a/crates/originweave-network/src/webdriver_bidi_session_end_response.rs +++ b/crates/originweave-network/src/webdriver_bidi_session_end_response.rs @@ -1,8 +1,9 @@ use std::{error::Error, fmt}; use crate::{ - WebDriverBiDiCommandCorrelation, WebDriverBiDiCommandCorrelationError, - WebDriverBiDiCommandKind, WebDriverBiDiCorrelatedResponseOutcome, WebDriverBiDiJsonEnvelope, + webdriver_bidi_connection::WebDriverBiDiConnectionGeneration, WebDriverBiDiCommandCorrelation, + WebDriverBiDiCommandCorrelationError, WebDriverBiDiCommandKind, + WebDriverBiDiCorrelatedResponseOutcome, WebDriverBiDiJsonEnvelope, WebDriverBiDiJsonEnvelopeError, WebDriverBiDiWebSocketTextMessage, }; @@ -20,7 +21,7 @@ use crate::{ #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub struct WebDriverBiDiSessionEndResult { command_id: u64, - connection_generation: u64, + connection_generation: WebDriverBiDiConnectionGeneration, } impl WebDriverBiDiSessionEndResult { @@ -68,7 +69,7 @@ impl WebDriverBiDiSessionEndResult { self.command_id } - pub(crate) const fn connection_generation(&self) -> u64 { + pub(crate) const fn connection_generation(&self) -> WebDriverBiDiConnectionGeneration { self.connection_generation } } From 4255eb498966df6168d1f3f4ff55b4238399f387 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 5 Sep 2026 15:50:32 +0900 Subject: [PATCH 047/105] refactor(network): type transport-closure connection provenance --- .../src/webdriver_bidi_websocket_transport_closure.rs | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/crates/originweave-network/src/webdriver_bidi_websocket_transport_closure.rs b/crates/originweave-network/src/webdriver_bidi_websocket_transport_closure.rs index 0f5be7b49..2da3ce4e7 100644 --- a/crates/originweave-network/src/webdriver_bidi_websocket_transport_closure.rs +++ b/crates/originweave-network/src/webdriver_bidi_websocket_transport_closure.rs @@ -1,6 +1,9 @@ use std::{error::Error, fmt, time::Duration}; -use crate::{WebDriverBiDiWebSocketEstablished, WebDriverBiDiWebSocketFrameError}; +use crate::{ + webdriver_bidi_connection::WebDriverBiDiConnectionGeneration, + WebDriverBiDiWebSocketEstablished, WebDriverBiDiWebSocketFrameError, +}; /// Bounded transport-closure condition observed on one consumed WebDriver BiDi WebSocket. /// @@ -28,7 +31,7 @@ pub enum WebDriverBiDiWebSocketTransportClosureKind { pub struct WebDriverBiDiWebSocketTransportClosureObservation { kind: WebDriverBiDiWebSocketTransportClosureKind, peer_close_status_code: Option, - connection_generation: u64, + connection_generation: WebDriverBiDiConnectionGeneration, } impl WebDriverBiDiWebSocketTransportClosureObservation { @@ -54,7 +57,7 @@ impl WebDriverBiDiWebSocketTransportClosureObservation { established: WebDriverBiDiWebSocketEstablished, frame_timeout: Duration, allow_pong: bool, - connection_generation: u64, + connection_generation: WebDriverBiDiConnectionGeneration, ) -> Result { match established.read_frame(frame_timeout) { Ok((established, frame)) if frame.opcode() == 0xa && allow_pong => Self::observe_frame( @@ -103,7 +106,7 @@ impl WebDriverBiDiWebSocketTransportClosureObservation { self.peer_close_status_code } - pub(crate) const fn connection_generation(&self) -> u64 { + pub(crate) const fn connection_generation(&self) -> WebDriverBiDiConnectionGeneration { self.connection_generation } } From 0a94765e0c631a043febec0973fd043304877537 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 5 Sep 2026 16:03:52 +0900 Subject: [PATCH 048/105] test(network): reproduce foreign BiDi response correlation --- ...sion_end_response_connection_provenance.rs | 151 ++++++++++++++++++ 1 file changed, 151 insertions(+) create mode 100644 crates/originweave-network/tests/webdriver_bidi_session_end_response_connection_provenance.rs diff --git a/crates/originweave-network/tests/webdriver_bidi_session_end_response_connection_provenance.rs b/crates/originweave-network/tests/webdriver_bidi_session_end_response_connection_provenance.rs new file mode 100644 index 000000000..24fb6f989 --- /dev/null +++ b/crates/originweave-network/tests/webdriver_bidi_session_end_response_connection_provenance.rs @@ -0,0 +1,151 @@ +use std::{ + error::Error, + io::{self, Read, Write}, + net::{SocketAddr, TcpListener, TcpStream}, + thread, + time::Duration, +}; + +use originweave_core::WebDriverBiDiWebSocketEndpoint; +use originweave_network::{ + WebDriverBiDiCommandCorrelation, WebDriverBiDiSessionEndCommand, + WebDriverBiDiSessionEndResult, WebDriverBiDiTcpConnectionPlan, + WebDriverBiDiWebSocketClientKey, WebDriverBiDiWebSocketHandshakePlan, + WebDriverBiDiWebSocketMaskKey, WebDriverBiDiWebSocketMessageAssembler, + WebDriverBiDiWebSocketMessageAssembly, WebDriverBiDiWebSocketTextMessage, +}; + +const SESSION_ID: &str = "01234567-89ab-cdef-0123-456789abcdef"; +const RFC6455_SAMPLE_KEY: &str = "dGhlIHNhbXBsZSBub25jZQ=="; +const OPENING_RESPONSE: &[u8] = b"HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=\r\n\r\n"; +const END_SUCCESS_RESPONSE: &[u8] = br#"{"type":"success","id":7,"result":{}}"#; + +fn read_opening_request(stream: &mut TcpStream) -> io::Result<()> { + stream.set_read_timeout(Some(Duration::from_secs(2)))?; + let mut request = Vec::new(); + let mut buffer = [0_u8; 512]; + while !request.ends_with(b"\r\n\r\n") { + let count = stream.read(&mut buffer)?; + if count == 0 { + return Err(io::Error::new( + io::ErrorKind::UnexpectedEof, + "client opening request ended before the header terminator", + )); + } + request.extend_from_slice(&buffer[..count]); + } + Ok(()) +} + +fn read_masked_text_frame(stream: &mut TcpStream) -> io::Result> { + let mut header = [0_u8; 2]; + stream.read_exact(&mut header)?; + if header[0] != 0x81 || header[1] & 0x80 == 0 { + return Err(io::Error::new( + io::ErrorKind::InvalidData, + "expected one final masked client text frame", + )); + } + let length = usize::from(header[1] & 0x7f); + if length > 125 { + return Err(io::Error::new( + io::ErrorKind::InvalidData, + "session.end command unexpectedly required extended framing", + )); + } + let mut mask = [0_u8; 4]; + stream.read_exact(&mut mask)?; + let mut payload = vec![0_u8; length]; + stream.read_exact(&mut payload)?; + for (index, byte) in payload.iter_mut().enumerate() { + *byte ^= mask[index % mask.len()]; + } + Ok(payload) +} + +fn establish( + local_addr: SocketAddr, +) -> Result> { + let endpoint = format!("ws://{local_addr}/session/{SESSION_ID}"); + let target = WebDriverBiDiWebSocketEndpoint::new(&endpoint)? + .correlate_session_id(SESSION_ID)? + .into_explicit_connect_target()?; + let connection = + WebDriverBiDiTcpConnectionPlan::new(target, Duration::from_secs(1), 1)?.connect()?; + let key = WebDriverBiDiWebSocketClientKey::new(RFC6455_SAMPLE_KEY)?; + Ok(WebDriverBiDiWebSocketHandshakePlan::new(connection, key)? + .write_opening_request(Duration::from_millis(500))? + .read_opening_response(Duration::from_millis(500))?) +} + +fn assemble_one_text( + established: originweave_network::WebDriverBiDiWebSocketEstablished, +) -> Result> { + let (_established, frame) = established.read_frame(Duration::from_millis(500))?; + let mut assembler = WebDriverBiDiWebSocketMessageAssembler::new(); + match assembler.push_frame(frame)? { + WebDriverBiDiWebSocketMessageAssembly::Text(text) => Ok(text), + other => Err(io::Error::other(format!( + "foreign response produced unexpected assembly state: {other:?}" + )) + .into()), + } +} + +#[test] +fn response_received_on_reconnected_transport_cannot_consume_prior_connection_command() +-> Result<(), Box> { + let listener = TcpListener::bind(("127.0.0.1", 0))?; + let local_addr = listener.local_addr()?; + let server = thread::spawn(move || -> io::Result<()> { + let (mut first, _) = listener.accept()?; + read_opening_request(&mut first)?; + first.write_all(OPENING_RESPONSE)?; + let command = read_masked_text_frame(&mut first)?; + if command != br#"{"id":7,"method":"session.end","params":{}}"# { + return Err(io::Error::new( + io::ErrorKind::InvalidData, + "unexpected session.end command on first connection", + )); + } + + let (mut second, _) = listener.accept()?; + read_opening_request(&mut second)?; + second.write_all(OPENING_RESPONSE)?; + second.write_all(&[0x81, END_SUCCESS_RESPONSE.len() as u8])?; + second.write_all(END_SUCCESS_RESPONSE)?; + Ok(()) + }); + + let first = establish(local_addr)?; + let mut correlation = WebDriverBiDiCommandCorrelation::new(); + let _first = WebDriverBiDiSessionEndCommand::new(7)?.send( + first, + &mut correlation, + WebDriverBiDiWebSocketMaskKey::new([1, 2, 3, 4]), + Duration::from_millis(500), + )?; + assert_eq!(correlation.outstanding_count(), 1); + + let second = establish(local_addr)?; + let foreign_response = assemble_one_text(second)?; + let parsed = WebDriverBiDiSessionEndResult::parse_and_correlate( + &foreign_response, + &mut correlation, + ); + + assert!( + parsed.is_err(), + "response received on connection B must not acknowledge connection A" + ); + assert_eq!( + correlation.outstanding_count(), + 1, + "foreign response must not consume connection A correlation" + ); + + server + .join() + .map_err(|_| io::Error::other("connection-provenance test server panicked"))??; + Ok(()) +} From 1c2749ea439f51438bc56e76b648a00eb19feeaa Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 5 Sep 2026 16:04:26 +0900 Subject: [PATCH 049/105] feat(network): bind received BiDi messages to connection --- .../src/webdriver_bidi_received_message.rs | 186 ++++++++++++++++++ 1 file changed, 186 insertions(+) create mode 100644 crates/originweave-network/src/webdriver_bidi_received_message.rs diff --git a/crates/originweave-network/src/webdriver_bidi_received_message.rs b/crates/originweave-network/src/webdriver_bidi_received_message.rs new file mode 100644 index 000000000..3ecea01c4 --- /dev/null +++ b/crates/originweave-network/src/webdriver_bidi_received_message.rs @@ -0,0 +1,186 @@ +use std::{error::Error, fmt, time::Duration}; + +use crate::{ + webdriver_bidi_connection::WebDriverBiDiConnectionGeneration, + WebDriverBiDiWebSocketControlMessage, WebDriverBiDiWebSocketEstablished, + WebDriverBiDiWebSocketFrameError, WebDriverBiDiWebSocketMessageAssembler, + WebDriverBiDiWebSocketMessageAssembly, WebDriverBiDiWebSocketMessageError, + WebDriverBiDiWebSocketTextMessage, +}; + +/// One complete WebDriver BiDi text message bound to the exact verified connection that read it. +/// +/// The connection generation is minted by the TCP connection owner and cannot be supplied by +/// callers. The wrapper therefore provides correlation provenance without granting browser, page, +/// policy, process, profile, or Agent authority. Debug output never exposes the message payload. +pub struct WebDriverBiDiReceivedTextMessage { + message: WebDriverBiDiWebSocketTextMessage, + connection_generation: WebDriverBiDiConnectionGeneration, +} + +impl fmt::Debug for WebDriverBiDiReceivedTextMessage { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter + .debug_struct("WebDriverBiDiReceivedTextMessage") + .field("payload_bytes", &self.message.as_str().len()) + .field("connection_bound", &true) + .finish() + } +} + +impl WebDriverBiDiReceivedTextMessage { + pub(crate) const fn message(&self) -> &WebDriverBiDiWebSocketTextMessage { + &self.message + } + + pub(crate) const fn connection_generation(&self) -> WebDriverBiDiConnectionGeneration { + self.connection_generation + } +} + +/// Stateful message reader that keeps RFC 6455 fragmentation on one verified BiDi connection. +/// +/// The reader owns both the live established connection and the message assembler. Every frame +/// admitted into a fragmented message is therefore read from that same non-cloneable connection; +/// callers cannot combine fragments from another socket and still obtain a connection-bound text +/// message. Interleaved control frames are surfaced without discarding partial message state. +pub struct WebDriverBiDiWebSocketMessageReader { + established: WebDriverBiDiWebSocketEstablished, + assembler: WebDriverBiDiWebSocketMessageAssembler, + connection_generation: WebDriverBiDiConnectionGeneration, +} + +impl fmt::Debug for WebDriverBiDiWebSocketMessageReader { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter + .debug_struct("WebDriverBiDiWebSocketMessageReader") + .field("connection_bound", &true) + .field("assembler", &self.assembler) + .finish() + } +} + +impl WebDriverBiDiWebSocketMessageReader { + /// Consume one established WebSocket into a connection-bound message reader. + #[must_use] + pub fn new(established: WebDriverBiDiWebSocketEstablished) -> Self { + let connection_generation = established.transport_evidence().connection_generation(); + Self { + established, + assembler: WebDriverBiDiWebSocketMessageAssembler::new(), + connection_generation, + } + } + + /// Read and admit exactly one frame while preserving connection-bound fragmentation state. + /// + /// `Pending` and `Control` outcomes return the same reader so the next frame cannot silently + /// switch sockets. A completed text message carries the private generation of this exact reader. + pub fn read_next( + self, + frame_timeout: Duration, + ) -> Result { + let Self { + established, + mut assembler, + connection_generation, + } = self; + let (established, frame) = established + .read_frame(frame_timeout) + .map_err(|source| WebDriverBiDiConnectionMessageReadError::Frame { source })?; + let assembly = assembler + .push_frame(frame) + .map_err(|source| WebDriverBiDiConnectionMessageReadError::Message { source })?; + let reader = Self { + established, + assembler, + connection_generation, + }; + Ok(match assembly { + WebDriverBiDiWebSocketMessageAssembly::Pending => { + WebDriverBiDiConnectionMessageRead::Pending(reader) + } + WebDriverBiDiWebSocketMessageAssembly::Text(message) => { + WebDriverBiDiConnectionMessageRead::Text { + reader, + message: WebDriverBiDiReceivedTextMessage { + message, + connection_generation, + }, + } + } + WebDriverBiDiWebSocketMessageAssembly::Control(message) => { + WebDriverBiDiConnectionMessageRead::Control { reader, message } + } + }) + } +} + +/// Outcome from one connection-bound WebDriver BiDi message-reader step. +pub enum WebDriverBiDiConnectionMessageRead { + /// A fragmented text message remains incomplete; continue with this same reader. + Pending(WebDriverBiDiWebSocketMessageReader), + /// One complete text message was assembled entirely on this reader's verified connection. + Text { + /// Reader retaining the same established connection for later messages. + reader: WebDriverBiDiWebSocketMessageReader, + /// Complete text message carrying non-forgeable connection provenance. + message: WebDriverBiDiReceivedTextMessage, + }, + /// An interleaved RFC 6455 control message was observed without discarding partial text state. + Control { + /// Reader retaining the same established connection and partial-message state. + reader: WebDriverBiDiWebSocketMessageReader, + /// Validated control message observed on this exact connection. + message: WebDriverBiDiWebSocketControlMessage, + }, +} + +impl fmt::Debug for WebDriverBiDiConnectionMessageRead { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Pending(_) => formatter.write_str("Pending(connection-bound reader)"), + Self::Text { message, .. } => formatter.debug_tuple("Text").field(message).finish(), + Self::Control { message, .. } => { + formatter.debug_tuple("Control").field(message).finish() + } + } + } +} + +/// Fail-closed frame or message error while reading one connection-bound BiDi message step. +#[derive(Debug)] +pub enum WebDriverBiDiConnectionMessageReadError { + /// The exact established connection failed while reading or validating one RFC 6455 frame. + Frame { + /// Underlying bounded frame failure. + source: WebDriverBiDiWebSocketFrameError, + }, + /// The connection-local message assembler rejected the frame sequence. + Message { + /// Underlying bounded message-assembly failure. + source: WebDriverBiDiWebSocketMessageError, + }, +} + +impl fmt::Display for WebDriverBiDiConnectionMessageReadError { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Frame { .. } => formatter.write_str( + "connection-bound WebDriver BiDi WebSocket frame read failed", + ), + Self::Message { .. } => formatter.write_str( + "connection-bound WebDriver BiDi WebSocket message assembly failed", + ), + } + } +} + +impl Error for WebDriverBiDiConnectionMessageReadError { + fn source(&self) -> Option<&(dyn Error + 'static)> { + match self { + Self::Frame { source } => Some(source), + Self::Message { source } => Some(source), + } + } +} From 256f6b3323e6f42c1e0fb0e1a085419fb9b79e0a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 5 Sep 2026 16:05:07 +0900 Subject: [PATCH 050/105] fix(network): reject foreign BiDi response transports --- .../src/webdriver_bidi_command_correlation.rs | 91 ++++++++++++++++++- 1 file changed, 89 insertions(+), 2 deletions(-) diff --git a/crates/originweave-network/src/webdriver_bidi_command_correlation.rs b/crates/originweave-network/src/webdriver_bidi_command_correlation.rs index 2bfdf6802..d42bae2e5 100644 --- a/crates/originweave-network/src/webdriver_bidi_command_correlation.rs +++ b/crates/originweave-network/src/webdriver_bidi_command_correlation.rs @@ -92,6 +92,16 @@ pub enum WebDriverBiDiCommandCorrelationError { /// Command family actually registered for the outstanding identifier. actual: WebDriverBiDiCommandKind, }, + /// A connection-bound consumer found an outstanding command with no connection provenance. + CommandConnectionProvenanceMissing { + /// Exact outstanding local command identifier. + command_id: u64, + }, + /// The response was received on a different verified connection from the outstanding command. + ResponseConnectionMismatch { + /// Exact outstanding local command identifier left untouched after rejection. + command_id: u64, + }, /// An event is not a command response and cannot consume correlation state. EventIsNotResponse, /// A protocol error with a `null` id cannot be attributed to one outstanding command. @@ -108,6 +118,12 @@ impl fmt::Display for WebDriverBiDiCommandCorrelationError { Self::CommandKindMismatch { .. } => { "WebDriver BiDi response command kind does not match the outstanding command" } + Self::CommandConnectionProvenanceMissing { .. } => { + "WebDriver BiDi outstanding command lacks connection provenance" + } + Self::ResponseConnectionMismatch { .. } => { + "WebDriver BiDi response arrived on a different connection" + } Self::EventIsNotResponse => "WebDriver BiDi event cannot be correlated as a response", Self::UncorrelatableErrorResponse => { "WebDriver BiDi error response has no correlatable command id" @@ -125,7 +141,8 @@ impl Error for WebDriverBiDiCommandCorrelationError {} /// to that outbound command. Connection-owning command adapters may additionally bind the private /// generation of the exact established transport before I/O. A success or correlatable error /// response consumes the id exactly once only through a matching typed consumer. Events, null-id -/// errors, and command-kind mismatches leave outstanding state untouched. This type performs no I/O, +/// errors, command-kind mismatches, missing connection provenance, and responses received on a +/// different verified connection leave outstanding state untouched. This type performs no I/O, /// retry, command serialization, browser authentication, or authority grant. Debug output reports /// only the outstanding-count summary; command identifiers, families, and generations remain /// private correlation state. @@ -225,7 +242,8 @@ impl WebDriverBiDiCommandCorrelation { /// /// Successful responses and error responses with ids consume exactly one matching command. /// Unknown ids and command-kind mismatches fail without consuming state. Events and null-id - /// errors fail before touching the map. + /// errors fail before touching the map. This generic path does not claim received-connection + /// provenance; connection-sensitive command owners must use their connection-bound path. pub fn correlate_response_for( &mut self, envelope: &WebDriverBiDiJsonEnvelope, @@ -253,6 +271,37 @@ impl WebDriverBiDiCommandCorrelation { } } + pub(crate) fn correlate_response_for_connection( + &mut self, + envelope: &WebDriverBiDiJsonEnvelope, + expected_kind: WebDriverBiDiCommandKind, + received_connection_generation: WebDriverBiDiConnectionGeneration, + ) -> Result { + match envelope.routing() { + WebDriverBiDiJsonEnvelopeRouting::Event => { + Err(WebDriverBiDiCommandCorrelationError::EventIsNotResponse) + } + WebDriverBiDiJsonEnvelopeRouting::CommandError { command_id: None } => { + Err(WebDriverBiDiCommandCorrelationError::UncorrelatableErrorResponse) + } + WebDriverBiDiJsonEnvelopeRouting::CommandError { + command_id: Some(command_id), + } => self.complete_on_connection( + command_id, + expected_kind, + WebDriverBiDiCorrelatedResponseOutcome::Error, + received_connection_generation, + ), + WebDriverBiDiJsonEnvelopeRouting::CommandSuccess { command_id } => self + .complete_on_connection( + command_id, + expected_kind, + WebDriverBiDiCorrelatedResponseOutcome::Success, + received_connection_generation, + ), + } + } + fn require_command_kind( &self, command_id: u64, @@ -286,6 +335,32 @@ impl WebDriverBiDiCommandCorrelation { connection_generation: outstanding.connection_generation, }) } + + fn complete_on_connection( + &mut self, + command_id: u64, + expected_kind: WebDriverBiDiCommandKind, + outcome: WebDriverBiDiCorrelatedResponseOutcome, + received_connection_generation: WebDriverBiDiConnectionGeneration, + ) -> Result { + let outstanding = self.require_command_kind(command_id, expected_kind)?; + let expected_connection_generation = outstanding.connection_generation.ok_or( + WebDriverBiDiCommandCorrelationError::CommandConnectionProvenanceMissing { + command_id, + }, + )?; + if expected_connection_generation != received_connection_generation { + return Err(WebDriverBiDiCommandCorrelationError::ResponseConnectionMismatch { + command_id, + }); + } + let _removed = self.outstanding.remove(&command_id); + Ok(WebDriverBiDiCorrelatedResponse { + command_id, + outcome, + connection_generation: Some(expected_connection_generation), + }) + } } #[cfg(test)] @@ -318,6 +393,18 @@ mod tests { }, "WebDriver BiDi response command kind does not match the outstanding command", ), + ( + WebDriverBiDiCommandCorrelationError::CommandConnectionProvenanceMissing { + command_id: 7, + }, + "WebDriver BiDi outstanding command lacks connection provenance", + ), + ( + WebDriverBiDiCommandCorrelationError::ResponseConnectionMismatch { + command_id: 7, + }, + "WebDriver BiDi response arrived on a different connection", + ), ( WebDriverBiDiCommandCorrelationError::EventIsNotResponse, "WebDriver BiDi event cannot be correlated as a response", From 9037db81a028b7b1843cc8aa8edfa501d1b33bfd Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 5 Sep 2026 16:05:37 +0900 Subject: [PATCH 051/105] fix(network): require received connection provenance for session.end --- .../webdriver_bidi_session_end_response.rs | 71 ++++++++++++++----- 1 file changed, 54 insertions(+), 17 deletions(-) diff --git a/crates/originweave-network/src/webdriver_bidi_session_end_response.rs b/crates/originweave-network/src/webdriver_bidi_session_end_response.rs index c1c9314d4..5e113fdd4 100644 --- a/crates/originweave-network/src/webdriver_bidi_session_end_response.rs +++ b/crates/originweave-network/src/webdriver_bidi_session_end_response.rs @@ -4,7 +4,7 @@ use crate::{ webdriver_bidi_connection::WebDriverBiDiConnectionGeneration, WebDriverBiDiCommandCorrelation, WebDriverBiDiCommandCorrelationError, WebDriverBiDiCommandKind, WebDriverBiDiCorrelatedResponseOutcome, WebDriverBiDiJsonEnvelope, - WebDriverBiDiJsonEnvelopeError, WebDriverBiDiWebSocketTextMessage, + WebDriverBiDiJsonEnvelopeError, WebDriverBiDiReceivedTextMessage, }; /// Typed protocol acknowledgment for one correlated WebDriver BiDi `session.end` command. @@ -13,9 +13,10 @@ use crate::{ /// local-end envelope parser already validates the complete JSON document and requires a success /// `result` object, so this command-specific boundary intentionally retains no generic result body /// and accepts extension members. The result retains the private process-local generation of the -/// exact connection on which the command was registered before I/O so later teardown evidence can -/// reject another connection even when both reuse the same WebDriver session and command id. This -/// value proves only that the remote end returned a correlated protocol success; it does not prove +/// exact connection on which the command was registered before I/O, and response admission requires +/// the complete received message to carry the same non-forgeable generation. This prevents another +/// verified connection from acknowledging the command even when both reuse the same WebDriver +/// session and command id. This value proves only a correlated protocol success; it does not prove /// Chromium process exit, profile deletion, resource release, or any other OriginWeave operational /// teardown postcondition. #[derive(Clone, Copy, Debug, Eq, PartialEq)] @@ -25,23 +26,40 @@ pub struct WebDriverBiDiSessionEndResult { } impl WebDriverBiDiSessionEndResult { - /// Parse one bounded local-end message and consume its exact outstanding command on response. + /// Parse one connection-bound local-end message and consume its exact outstanding command. /// /// Complete JSON and common WebDriver BiDi envelope validation occur before correlation state - /// can be consumed. Successful responses require the connection generation registered by the - /// typed `session.end` sender and retain only that private provenance plus the matched command - /// id. A correlatable protocol-error response consumes its matching id and returns a typed remote - /// failure, while events, null-id errors, malformed envelopes, unknown ids, and command-kind - /// mismatches fail closed without consuming unrelated outstanding state. + /// can be consumed. The response must have been assembled by the connection-bound message reader + /// on the same private transport generation registered by the typed `session.end` sender. + /// Connection mismatch or missing command provenance fails without consuming the outstanding id. + /// A correlatable protocol-error response on the correct connection consumes its matching id and + /// returns a typed remote failure, while events, null-id errors, malformed envelopes, unknown ids, + /// and command-kind mismatches leave unrelated outstanding state untouched. pub fn parse_and_correlate( - message: &WebDriverBiDiWebSocketTextMessage, + message: &WebDriverBiDiReceivedTextMessage, correlation: &mut WebDriverBiDiCommandCorrelation, ) -> Result { - let envelope = WebDriverBiDiJsonEnvelope::parse(message) + let envelope = WebDriverBiDiJsonEnvelope::parse(message.message()) .map_err(|source| WebDriverBiDiSessionEndResponseError::Envelope { source })?; let completed = correlation - .correlate_response_for(&envelope, WebDriverBiDiCommandKind::SessionEnd) - .map_err(|source| WebDriverBiDiSessionEndResponseError::Correlation { source })?; + .correlate_response_for_connection( + &envelope, + WebDriverBiDiCommandKind::SessionEnd, + message.connection_generation(), + ) + .map_err(|source| match source { + WebDriverBiDiCommandCorrelationError::CommandConnectionProvenanceMissing { + command_id, + } => WebDriverBiDiSessionEndResponseError::MissingConnectionProvenance { + command_id, + }, + WebDriverBiDiCommandCorrelationError::ResponseConnectionMismatch { command_id } => { + WebDriverBiDiSessionEndResponseError::TransportConnectionMismatch { + command_id, + } + } + source => WebDriverBiDiSessionEndResponseError::Correlation { source }, + })?; match completed.outcome() { WebDriverBiDiCorrelatedResponseOutcome::Success => { @@ -87,9 +105,14 @@ pub enum WebDriverBiDiSessionEndResponseError { /// Exact typed correlation failure. source: WebDriverBiDiCommandCorrelationError, }, - /// A successful response consumed a `session.end` correlation that lacked transport provenance. + /// The outstanding `session.end` command lacked connection provenance required for admission. MissingConnectionProvenance { - /// Exact local command identifier whose registration omitted the connection generation. + /// Exact local command identifier left outstanding after rejection. + command_id: u64, + }, + /// The response was received on a different verified transport from the outstanding command. + TransportConnectionMismatch { + /// Exact local command identifier left outstanding after rejection. command_id: u64, }, /// The remote end returned a correlatable WebDriver BiDi protocol error for this command. @@ -110,6 +133,9 @@ impl fmt::Display for WebDriverBiDiSessionEndResponseError { } Self::MissingConnectionProvenance { .. } => formatter .write_str("WebDriver BiDi session.end response lacks connection provenance"), + Self::TransportConnectionMismatch { .. } => formatter.write_str( + "WebDriver BiDi session.end response arrived on a different connection", + ), Self::RemoteProtocolError { .. } => { formatter.write_str("WebDriver BiDi session.end returned a protocol error") } @@ -122,7 +148,9 @@ impl Error for WebDriverBiDiSessionEndResponseError { match self { Self::Envelope { source } => Some(source), Self::Correlation { source } => Some(source), - Self::MissingConnectionProvenance { .. } | Self::RemoteProtocolError { .. } => None, + Self::MissingConnectionProvenance { .. } + | Self::TransportConnectionMismatch { .. } + | Self::RemoteProtocolError { .. } => None, } } } @@ -160,6 +188,15 @@ mod tests { ); assert!(missing.source().is_none()); + let mismatch = WebDriverBiDiSessionEndResponseError::TransportConnectionMismatch { + command_id: 7, + }; + assert_eq!( + mismatch.to_string(), + "WebDriver BiDi session.end response arrived on a different connection" + ); + assert!(mismatch.source().is_none()); + let remote = WebDriverBiDiSessionEndResponseError::RemoteProtocolError { command_id: 7 }; assert_eq!( remote.to_string(), From b35a2484212002a31914504f8b125fc4bab976f1 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 5 Sep 2026 16:06:00 +0900 Subject: [PATCH 052/105] feat(network): expose connection-bound BiDi message reader --- crates/originweave-network/src/lib.rs | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/crates/originweave-network/src/lib.rs b/crates/originweave-network/src/lib.rs index 4c05319e4..6012bd124 100644 --- a/crates/originweave-network/src/lib.rs +++ b/crates/originweave-network/src/lib.rs @@ -7,14 +7,14 @@ //! `originweave-core` into one bounded exact TCP connection, binds and validates //! the RFC 6455 opening exchange, provides bounded masked client writes and //! unmasked server-frame reads, assembles bounded WebDriver BiDi text messages, -//! classifies complete local-end JSON envelopes, tracks bounded command-response -//! correlation, sends narrowly typed `session.status` and `session.end` commands, -//! admits typed correlated status and end responses, binds `session.end` ACK and -//! closure evidence to one private process-local connection generation, observes -//! bounded peer Close or clean-EOF transport cessation, and keeps protocol/transport -//! evidence separate from explicit operational teardown observations without exposing -//! generic JSON bodies or granting browser, TLS, policy, secret, process, profile, -//! or Agent authority. +//! binds received fragmented text to one exact verified connection, classifies +//! complete local-end JSON envelopes, tracks bounded command-response correlation, +//! sends narrowly typed `session.status` and `session.end` commands, admits typed +//! correlated status and end responses, binds `session.end` ACK and closure evidence +//! to one private process-local connection generation, observes bounded peer Close +//! or clean-EOF transport cessation, and keeps protocol/transport evidence separate +//! from explicit operational teardown observations without exposing generic JSON +//! bodies or granting browser, TLS, policy, secret, process, profile, or Agent authority. #![forbid(unsafe_code)] #![deny(missing_docs)] @@ -23,6 +23,7 @@ mod connection; mod webdriver_bidi_command_correlation; mod webdriver_bidi_connection; mod webdriver_bidi_json_envelope; +mod webdriver_bidi_received_message; mod webdriver_bidi_session_end_command; mod webdriver_bidi_session_end_response; mod webdriver_bidi_session_status_command; @@ -55,6 +56,10 @@ pub use webdriver_bidi_json_envelope::{ MAX_WEBDRIVER_BIDI_JS_UINT, MAX_WEBDRIVER_BIDI_JSON_DEPTH, WebDriverBiDiJsonEnvelope, WebDriverBiDiJsonEnvelopeError, WebDriverBiDiJsonEnvelopeKind, }; +pub use webdriver_bidi_received_message::{ + WebDriverBiDiConnectionMessageRead, WebDriverBiDiConnectionMessageReadError, + WebDriverBiDiReceivedTextMessage, WebDriverBiDiWebSocketMessageReader, +}; pub use webdriver_bidi_session_end_command::{ WebDriverBiDiSessionEndCommand, WebDriverBiDiSessionEndCommandError, }; From 98fe192dfa3a0080a23a54a72e3dfae02860b88d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 5 Sep 2026 16:06:45 +0900 Subject: [PATCH 053/105] test(network): bind session.end responses to receive stream --- .../webdriver_bidi_session_end_response.rs | 34 +++++++++++-------- 1 file changed, 20 insertions(+), 14 deletions(-) diff --git a/crates/originweave-network/tests/webdriver_bidi_session_end_response.rs b/crates/originweave-network/tests/webdriver_bidi_session_end_response.rs index f35d012e6..373a36437 100644 --- a/crates/originweave-network/tests/webdriver_bidi_session_end_response.rs +++ b/crates/originweave-network/tests/webdriver_bidi_session_end_response.rs @@ -8,12 +8,12 @@ use std::{ use originweave_core::WebDriverBiDiWebSocketEndpoint; use originweave_network::{ - WebDriverBiDiCommandCorrelation, WebDriverBiDiCommandKind, WebDriverBiDiSessionEndCommand, + WebDriverBiDiCommandCorrelation, WebDriverBiDiCommandKind, WebDriverBiDiConnectionMessageRead, + WebDriverBiDiReceivedTextMessage, WebDriverBiDiSessionEndCommand, WebDriverBiDiSessionEndResponseError, WebDriverBiDiSessionEndResult, WebDriverBiDiTcpConnectionPlan, WebDriverBiDiWebSocketClientKey, WebDriverBiDiWebSocketHandshakePlan, WebDriverBiDiWebSocketMaskKey, - WebDriverBiDiWebSocketMessageAssembler, WebDriverBiDiWebSocketMessageAssembly, - WebDriverBiDiWebSocketTextMessage, + WebDriverBiDiWebSocketMessageReader, }; const SESSION_ID: &str = "01234567-89ab-cdef-0123-456789abcdef"; @@ -74,7 +74,7 @@ fn send_end_and_read_response( response: &'static [u8], ) -> Result< ( - WebDriverBiDiWebSocketTextMessage, + WebDriverBiDiReceivedTextMessage, WebDriverBiDiCommandCorrelation, ), Box, @@ -115,15 +115,17 @@ fn send_end_and_read_response( Duration::from_millis(500), )?; - let (_established, frame) = established.read_frame(Duration::from_millis(500))?; - let mut assembler = WebDriverBiDiWebSocketMessageAssembler::new(); - let text = match assembler.push_frame(frame)? { - WebDriverBiDiWebSocketMessageAssembly::Text(text) => text, - other => { - return Err(io::Error::other(format!( - "session.end response produced unexpected assembly state: {other:?}" - )) - .into()); + let mut reader = WebDriverBiDiWebSocketMessageReader::new(established); + let text = loop { + match reader.read_next(Duration::from_millis(500))? { + WebDriverBiDiConnectionMessageRead::Pending(next) => reader = next, + WebDriverBiDiConnectionMessageRead::Text { message, .. } => break message, + WebDriverBiDiConnectionMessageRead::Control { message, .. } => { + return Err(io::Error::other(format!( + "session.end response produced unexpected control message: {message:?}" + )) + .into()); + } } }; @@ -164,7 +166,11 @@ fn session_end_success_rejects_correlation_without_connection_provenance() error.to_string(), "WebDriver BiDi session.end response lacks connection provenance" ); - assert_eq!(unbound.outstanding_count(), 0); + assert_eq!( + unbound.outstanding_count(), + 1, + "missing provenance must not consume the outstanding command" + ); Ok(()) } From d4cd2de94b8ad4b35eb791072e3c0581161a3730 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 5 Sep 2026 16:07:11 +0900 Subject: [PATCH 054/105] test(network): assert exact received transport mismatch --- ...sion_end_response_connection_provenance.rs | 50 ++++++++++++------- 1 file changed, 31 insertions(+), 19 deletions(-) diff --git a/crates/originweave-network/tests/webdriver_bidi_session_end_response_connection_provenance.rs b/crates/originweave-network/tests/webdriver_bidi_session_end_response_connection_provenance.rs index 24fb6f989..be5dbade5 100644 --- a/crates/originweave-network/tests/webdriver_bidi_session_end_response_connection_provenance.rs +++ b/crates/originweave-network/tests/webdriver_bidi_session_end_response_connection_provenance.rs @@ -8,11 +8,12 @@ use std::{ use originweave_core::WebDriverBiDiWebSocketEndpoint; use originweave_network::{ - WebDriverBiDiCommandCorrelation, WebDriverBiDiSessionEndCommand, - WebDriverBiDiSessionEndResult, WebDriverBiDiTcpConnectionPlan, - WebDriverBiDiWebSocketClientKey, WebDriverBiDiWebSocketHandshakePlan, - WebDriverBiDiWebSocketMaskKey, WebDriverBiDiWebSocketMessageAssembler, - WebDriverBiDiWebSocketMessageAssembly, WebDriverBiDiWebSocketTextMessage, + WebDriverBiDiCommandCorrelation, WebDriverBiDiConnectionMessageRead, + WebDriverBiDiReceivedTextMessage, WebDriverBiDiSessionEndCommand, + WebDriverBiDiSessionEndResponseError, WebDriverBiDiSessionEndResult, + WebDriverBiDiTcpConnectionPlan, WebDriverBiDiWebSocketClientKey, + WebDriverBiDiWebSocketHandshakePlan, WebDriverBiDiWebSocketMaskKey, + WebDriverBiDiWebSocketMessageReader, }; const SESSION_ID: &str = "01234567-89ab-cdef-0123-456789abcdef"; @@ -78,17 +79,21 @@ fn establish( .read_opening_response(Duration::from_millis(500))?) } -fn assemble_one_text( +fn read_one_connection_bound_text( established: originweave_network::WebDriverBiDiWebSocketEstablished, -) -> Result> { - let (_established, frame) = established.read_frame(Duration::from_millis(500))?; - let mut assembler = WebDriverBiDiWebSocketMessageAssembler::new(); - match assembler.push_frame(frame)? { - WebDriverBiDiWebSocketMessageAssembly::Text(text) => Ok(text), - other => Err(io::Error::other(format!( - "foreign response produced unexpected assembly state: {other:?}" - )) - .into()), +) -> Result> { + let mut reader = WebDriverBiDiWebSocketMessageReader::new(established); + loop { + match reader.read_next(Duration::from_millis(500))? { + WebDriverBiDiConnectionMessageRead::Pending(next) => reader = next, + WebDriverBiDiConnectionMessageRead::Text { message, .. } => return Ok(message), + WebDriverBiDiConnectionMessageRead::Control { message, .. } => { + return Err(io::Error::other(format!( + "foreign response produced unexpected control message: {message:?}" + )) + .into()); + } + } } } @@ -128,15 +133,22 @@ fn response_received_on_reconnected_transport_cannot_consume_prior_connection_co assert_eq!(correlation.outstanding_count(), 1); let second = establish(local_addr)?; - let foreign_response = assemble_one_text(second)?; + let foreign_response = read_one_connection_bound_text(second)?; let parsed = WebDriverBiDiSessionEndResult::parse_and_correlate( &foreign_response, &mut correlation, ); + let error = parsed + .err() + .ok_or_else(|| io::Error::other("foreign response acknowledged prior connection"))?; - assert!( - parsed.is_err(), - "response received on connection B must not acknowledge connection A" + assert!(matches!( + error, + WebDriverBiDiSessionEndResponseError::TransportConnectionMismatch { command_id: 7 } + )); + assert_eq!( + error.to_string(), + "WebDriver BiDi session.end response arrived on a different connection" ); assert_eq!( correlation.outstanding_count(), From ccefd011668aaac3025169229fcec96f48876ffe Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 5 Sep 2026 16:07:55 +0900 Subject: [PATCH 055/105] refactor(network): return transport after complete BiDi message --- .../src/webdriver_bidi_received_message.rs | 32 ++++++++++++------- 1 file changed, 20 insertions(+), 12 deletions(-) diff --git a/crates/originweave-network/src/webdriver_bidi_received_message.rs b/crates/originweave-network/src/webdriver_bidi_received_message.rs index 3ecea01c4..671810942 100644 --- a/crates/originweave-network/src/webdriver_bidi_received_message.rs +++ b/crates/originweave-network/src/webdriver_bidi_received_message.rs @@ -75,7 +75,9 @@ impl WebDriverBiDiWebSocketMessageReader { /// Read and admit exactly one frame while preserving connection-bound fragmentation state. /// /// `Pending` and `Control` outcomes return the same reader so the next frame cannot silently - /// switch sockets. A completed text message carries the private generation of this exact reader. + /// switch sockets. A completed text message carries the private generation of this exact reader; + /// because a completed text leaves no partial fragments, its established transport is returned + /// directly for the next protocol stage rather than exposing a way to discard partial state. pub fn read_next( self, frame_timeout: Duration, @@ -91,18 +93,17 @@ impl WebDriverBiDiWebSocketMessageReader { let assembly = assembler .push_frame(frame) .map_err(|source| WebDriverBiDiConnectionMessageReadError::Message { source })?; - let reader = Self { - established, - assembler, - connection_generation, - }; Ok(match assembly { WebDriverBiDiWebSocketMessageAssembly::Pending => { - WebDriverBiDiConnectionMessageRead::Pending(reader) + WebDriverBiDiConnectionMessageRead::Pending(Self { + established, + assembler, + connection_generation, + }) } WebDriverBiDiWebSocketMessageAssembly::Text(message) => { WebDriverBiDiConnectionMessageRead::Text { - reader, + established, message: WebDriverBiDiReceivedTextMessage { message, connection_generation, @@ -110,7 +111,14 @@ impl WebDriverBiDiWebSocketMessageReader { } } WebDriverBiDiWebSocketMessageAssembly::Control(message) => { - WebDriverBiDiConnectionMessageRead::Control { reader, message } + WebDriverBiDiConnectionMessageRead::Control { + reader: Self { + established, + assembler, + connection_generation, + }, + message, + } } }) } @@ -120,10 +128,10 @@ impl WebDriverBiDiWebSocketMessageReader { pub enum WebDriverBiDiConnectionMessageRead { /// A fragmented text message remains incomplete; continue with this same reader. Pending(WebDriverBiDiWebSocketMessageReader), - /// One complete text message was assembled entirely on this reader's verified connection. + /// One complete text message was assembled entirely on this verified connection. Text { - /// Reader retaining the same established connection for later messages. - reader: WebDriverBiDiWebSocketMessageReader, + /// Established transport returned after the complete message left no partial fragments. + established: WebDriverBiDiWebSocketEstablished, /// Complete text message carrying non-forgeable connection provenance. message: WebDriverBiDiReceivedTextMessage, }, From 59e92c64c7274d73ba59045c811954a256ebb6d2 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 5 Sep 2026 16:08:35 +0900 Subject: [PATCH 056/105] test(network): preserve transport through bound response read --- ...driver_bidi_session_teardown_assessment.rs | 31 +++++++++++-------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/crates/originweave-network/tests/webdriver_bidi_session_teardown_assessment.rs b/crates/originweave-network/tests/webdriver_bidi_session_teardown_assessment.rs index 4a90805cf..d17dc159a 100644 --- a/crates/originweave-network/tests/webdriver_bidi_session_teardown_assessment.rs +++ b/crates/originweave-network/tests/webdriver_bidi_session_teardown_assessment.rs @@ -8,14 +8,14 @@ use std::{ use originweave_core::WebDriverBiDiWebSocketEndpoint; use originweave_network::{ - WebDriverBiDiCommandCorrelation, WebDriverBiDiSessionEndCommand, WebDriverBiDiSessionEndResult, + WebDriverBiDiCommandCorrelation, WebDriverBiDiConnectionMessageRead, + WebDriverBiDiSessionEndCommand, WebDriverBiDiSessionEndResult, WebDriverBiDiSessionTeardownAssessment, WebDriverBiDiSessionTeardownAssessmentError, WebDriverBiDiSessionTeardownDisposition, WebDriverBiDiSessionTeardownObservations, WebDriverBiDiTcpConnectionPlan, WebDriverBiDiWebSocketClientKey, WebDriverBiDiWebSocketEstablished, WebDriverBiDiWebSocketHandshakePlan, - WebDriverBiDiWebSocketMaskKey, WebDriverBiDiWebSocketMessageAssembler, - WebDriverBiDiWebSocketMessageAssembly, WebDriverBiDiWebSocketTransportClosureKind, - WebDriverBiDiWebSocketTransportClosureObservation, + WebDriverBiDiWebSocketMaskKey, WebDriverBiDiWebSocketMessageReader, + WebDriverBiDiWebSocketTransportClosureKind, WebDriverBiDiWebSocketTransportClosureObservation, }; const SESSION_ID: &str = "01234567-89ab-cdef-0123-456789abcdef"; @@ -114,15 +114,20 @@ fn correlated_session_end_ack_and_transport_on( WebDriverBiDiWebSocketMaskKey::new([1, 2, 3, 4]), Duration::from_millis(500), )?; - let (established, frame) = established.read_frame(Duration::from_millis(500))?; - let mut assembler = WebDriverBiDiWebSocketMessageAssembler::new(); - let text = match assembler.push_frame(frame)? { - WebDriverBiDiWebSocketMessageAssembly::Text(text) => text, - other => { - return Err(io::Error::other(format!( - "session.end response produced unexpected assembly state: {other:?}" - )) - .into()); + let mut reader = WebDriverBiDiWebSocketMessageReader::new(established); + let (established, text) = loop { + match reader.read_next(Duration::from_millis(500))? { + WebDriverBiDiConnectionMessageRead::Pending(next) => reader = next, + WebDriverBiDiConnectionMessageRead::Text { + established, + message, + } => break (established, message), + WebDriverBiDiConnectionMessageRead::Control { message, .. } => { + return Err(io::Error::other(format!( + "session.end response produced unexpected control message: {message:?}" + )) + .into()); + } } }; let acknowledged = WebDriverBiDiSessionEndResult::parse_and_correlate(&text, &mut correlation)?; From e7bfec4488b7cb4776df7b546cacb46c8c9eb13e Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 5 Sep 2026 16:09:25 +0900 Subject: [PATCH 057/105] test(network): cover connection-bound message reader --- .../tests/webdriver_bidi_received_message.rs | 137 ++++++++++++++++++ 1 file changed, 137 insertions(+) create mode 100644 crates/originweave-network/tests/webdriver_bidi_received_message.rs diff --git a/crates/originweave-network/tests/webdriver_bidi_received_message.rs b/crates/originweave-network/tests/webdriver_bidi_received_message.rs new file mode 100644 index 000000000..edf752c06 --- /dev/null +++ b/crates/originweave-network/tests/webdriver_bidi_received_message.rs @@ -0,0 +1,137 @@ +use std::{ + error::Error, + io::{self, Read, Write}, + net::{TcpListener, TcpStream}, + thread, + time::Duration, +}; + +use originweave_core::WebDriverBiDiWebSocketEndpoint; +use originweave_network::{ + WebDriverBiDiConnectionMessageRead, WebDriverBiDiConnectionMessageReadError, + WebDriverBiDiTcpConnectionPlan, WebDriverBiDiWebSocketClientKey, + WebDriverBiDiWebSocketHandshakePlan, WebDriverBiDiWebSocketMessageReader, +}; + +const SESSION_ID: &str = "01234567-89ab-cdef-0123-456789abcdef"; +const RFC6455_SAMPLE_KEY: &str = "dGhlIHNhbXBsZSBub25jZQ=="; +const OPENING_RESPONSE: &[u8] = b"HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=\r\n\r\n"; + +fn read_opening_request(stream: &mut TcpStream) -> io::Result<()> { + stream.set_read_timeout(Some(Duration::from_secs(2)))?; + let mut request = Vec::new(); + let mut buffer = [0_u8; 512]; + while !request.ends_with(b"\r\n\r\n") { + let count = stream.read(&mut buffer)?; + if count == 0 { + return Err(io::Error::new( + io::ErrorKind::UnexpectedEof, + "client opening request ended before the header terminator", + )); + } + request.extend_from_slice(&buffer[..count]); + } + Ok(()) +} + +fn established_for_frames( + frames: Vec>, +) -> Result> { + let listener = TcpListener::bind(("127.0.0.1", 0))?; + let local_addr = listener.local_addr()?; + thread::spawn(move || -> io::Result<()> { + let (mut stream, _) = listener.accept()?; + read_opening_request(&mut stream)?; + stream.write_all(OPENING_RESPONSE)?; + for frame in frames { + stream.write_all(&frame)?; + } + Ok(()) + }); + + let endpoint = format!("ws://{local_addr}/session/{SESSION_ID}"); + let target = WebDriverBiDiWebSocketEndpoint::new(&endpoint)? + .correlate_session_id(SESSION_ID)? + .into_explicit_connect_target()?; + let connection = + WebDriverBiDiTcpConnectionPlan::new(target, Duration::from_secs(1), 1)?.connect()?; + let key = WebDriverBiDiWebSocketClientKey::new(RFC6455_SAMPLE_KEY)?; + Ok(WebDriverBiDiWebSocketHandshakePlan::new(connection, key)? + .write_opening_request(Duration::from_millis(500))? + .read_opening_response(Duration::from_millis(500))?) +} + +#[test] +fn fragmented_text_and_interleaved_control_remain_on_one_reader() -> Result<(), Box> { + let established = established_for_frames(vec![ + vec![0x01, 0x03, b'a', b'b', b'c'], + vec![0x89, 0x00], + vec![0x80, 0x03, b'd', b'e', b'f'], + ])?; + let reader = WebDriverBiDiWebSocketMessageReader::new(established); + assert!(format!("{reader:?}").contains("connection_bound")); + + let first = reader.read_next(Duration::from_millis(500))?; + assert!(format!("{first:?}").starts_with("Pending")); + let reader = match first { + WebDriverBiDiConnectionMessageRead::Pending(reader) => reader, + _ => return Err(io::Error::other("first fragment did not remain pending").into()), + }; + + let control = reader.read_next(Duration::from_millis(500))?; + assert!(format!("{control:?}").starts_with("Control")); + let reader = match control { + WebDriverBiDiConnectionMessageRead::Control { reader, message } => { + assert_eq!(message.payload(), b""); + reader + } + _ => return Err(io::Error::other("interleaved Ping was not surfaced").into()), + }; + + let completed = reader.read_next(Duration::from_millis(500))?; + let debug = format!("{completed:?}"); + assert!(debug.starts_with("Text")); + assert!(debug.contains("payload_bytes")); + match completed { + WebDriverBiDiConnectionMessageRead::Text { + established, + message: _, + } => drop(established), + _ => return Err(io::Error::other("continuation did not complete text message").into()), + } + Ok(()) +} + +#[test] +fn frame_and_message_failures_remain_typed_and_sourced() -> Result<(), Box> { + let malformed = established_for_frames(vec![vec![0x81, 0x80]])?; + let frame_error = WebDriverBiDiWebSocketMessageReader::new(malformed) + .read_next(Duration::from_millis(500)) + .err() + .ok_or_else(|| io::Error::other("masked server frame was accepted"))?; + assert!(matches!( + frame_error, + WebDriverBiDiConnectionMessageReadError::Frame { .. } + )); + assert_eq!( + frame_error.to_string(), + "connection-bound WebDriver BiDi WebSocket frame read failed" + ); + assert!(frame_error.source().is_some()); + + let binary = established_for_frames(vec![vec![0x82, 0x00]])?; + let message_error = WebDriverBiDiWebSocketMessageReader::new(binary) + .read_next(Duration::from_millis(500)) + .err() + .ok_or_else(|| io::Error::other("binary BiDi message was accepted"))?; + assert!(matches!( + message_error, + WebDriverBiDiConnectionMessageReadError::Message { .. } + )); + assert_eq!( + message_error.to_string(), + "connection-bound WebDriver BiDi WebSocket message assembly failed" + ); + assert!(message_error.source().is_some()); + Ok(()) +} From f4e18ecb37fc670467b64ddfb2e580461ea02acd Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 5 Sep 2026 16:10:51 +0900 Subject: [PATCH 058/105] docs(network): trace received BiDi connection provenance --- ...received-response-connection-provenance.md | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 docs/doctoring/webdriver-bidi-received-response-connection-provenance.md diff --git a/docs/doctoring/webdriver-bidi-received-response-connection-provenance.md b/docs/doctoring/webdriver-bidi-received-response-connection-provenance.md new file mode 100644 index 000000000..e565e343b --- /dev/null +++ b/docs/doctoring/webdriver-bidi-received-response-connection-provenance.md @@ -0,0 +1,35 @@ +# WebDriver BiDi received-response connection provenance + +Status: Draft implementation evidence for PR #255. This note is not an ADR and does not grant browser, policy, process, profile, or Agent authority. + +## Problem + +A WebDriver BiDi command id is local-end correlation data, not transport identity. Before this repair, `session.end` stored the private generation of connection A when sending command id `7`, but response parsing accepted a bare assembled text message. A valid response with id `7` read from a separately verified connection B could therefore consume A's outstanding command and inherit A's stored generation. Later teardown evidence could appear internally consistent even though the protocol acknowledgment actually arrived on B. + +The test-first commit `0a94765e0c631a043febec0973fd043304877537` reproduces this with two real loopback TCP/WebSocket connections using the same WebDriver session id and command id. On the then-current implementation the foreign response was accepted and the A correlation was consumed. This is source-level RED lineage; no hosted RED is claimed because later repair commits superseded that exact generation before terminal hosted execution. + +## Constraints and rejected alternatives + +WebDriver BiDi uses the command `id` only so the local end can identify a command response; commands may finish out of order, and the id is opaque to the remote end. The current specification's command-processing algorithm sends the resulting WebSocket message over the same `connection` on which the command was received. OriginWeave therefore needs local evidence of the receiving connection in addition to the response id. + +Passing a caller-supplied connection generation into response parsing was rejected because it would turn provenance into forgeable metadata. Passing a bare assembled text message plus a separately supplied established connection was also rejected because callers could accidentally pair a message from B with connection A. Adding a connection id to JSON was rejected because it would invent protocol data not defined by WebDriver BiDi. + +## Selected boundary + +`WebDriverBiDiWebSocketMessageReader` consumes one established WebSocket and owns its message assembler. Every text fragment admitted by that reader is therefore read from the same non-cloneable verified transport. `Pending` and interleaved control outcomes retain the reader so fragmented state cannot be moved onto another connection. Once a text message is complete, the reader emits `WebDriverBiDiReceivedTextMessage` carrying the private process-local connection generation and returns the established transport separately because no partial fragments remain. + +`session.end` response admission now requires that received-message type. Correlation validates command kind, stored connection provenance, and received connection generation before removing the outstanding id. Missing provenance and a different received connection both fail closed without consuming the command. Protocol ACK remains distinct from transport closure, Chromium process exit, profile deletion, and task completion. + +This placement also preserves RFC 6455 fragmentation semantics: a message may span multiple frames, control frames may appear between fragments, recipients must support fragmented and unfragmented messages, and message fragments are delivered in order on the WebSocket connection. The reader therefore binds the assembled-message transaction to one connection rather than treating individual application payloads as free-standing evidence. + +## Evidence and remaining risk + +The repair adds a realistic two-connection regression for a foreign `session.end` success response and focused reader coverage for fragmented text, an interleaved control frame, malformed server framing, and binary-message rejection. Exact hosted CI/coverage/security results belong to the live PR head and must be read from GitHub; predecessor results do not transfer. + +The current scope deliberately does not infer browser-process ownership from the WebSocket connection and does not make protocol success an operational teardown post-condition. Process-exit and profile-removal evidence remain unavailable until their canonical runtime owners provide non-forgeable contracts. + +## References + +Fette, I., & Melnikov, A. (2011). *The WebSocket Protocol* (RFC 6455). Internet Engineering Task Force. https://www.rfc-editor.org/rfc/rfc6455 + +W3C Browser Testing and Tools Working Group. (2026). *WebDriver BiDi*. World Wide Web Consortium. https://w3c.github.io/webdriver-bidi/ From feba41f13fb39e83fc8b377b8a55bd7c27348f9d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 5 Sep 2026 16:15:18 +0900 Subject: [PATCH 059/105] test(network): cover uncorrelatable connection-bound responses --- .../webdriver_bidi_session_end_response.rs | 53 ++++++++++++++++--- 1 file changed, 47 insertions(+), 6 deletions(-) diff --git a/crates/originweave-network/tests/webdriver_bidi_session_end_response.rs b/crates/originweave-network/tests/webdriver_bidi_session_end_response.rs index 373a36437..602577500 100644 --- a/crates/originweave-network/tests/webdriver_bidi_session_end_response.rs +++ b/crates/originweave-network/tests/webdriver_bidi_session_end_response.rs @@ -8,12 +8,12 @@ use std::{ use originweave_core::WebDriverBiDiWebSocketEndpoint; use originweave_network::{ - WebDriverBiDiCommandCorrelation, WebDriverBiDiCommandKind, WebDriverBiDiConnectionMessageRead, - WebDriverBiDiReceivedTextMessage, WebDriverBiDiSessionEndCommand, - WebDriverBiDiSessionEndResponseError, WebDriverBiDiSessionEndResult, - WebDriverBiDiTcpConnectionPlan, WebDriverBiDiWebSocketClientKey, - WebDriverBiDiWebSocketHandshakePlan, WebDriverBiDiWebSocketMaskKey, - WebDriverBiDiWebSocketMessageReader, + WebDriverBiDiCommandCorrelation, WebDriverBiDiCommandCorrelationError, WebDriverBiDiCommandKind, + WebDriverBiDiConnectionMessageRead, WebDriverBiDiReceivedTextMessage, + WebDriverBiDiSessionEndCommand, WebDriverBiDiSessionEndResponseError, + WebDriverBiDiSessionEndResult, WebDriverBiDiTcpConnectionPlan, + WebDriverBiDiWebSocketClientKey, WebDriverBiDiWebSocketHandshakePlan, + WebDriverBiDiWebSocketMaskKey, WebDriverBiDiWebSocketMessageReader, }; const SESSION_ID: &str = "01234567-89ab-cdef-0123-456789abcdef"; @@ -23,6 +23,10 @@ const END_SUCCESS_RESPONSE: &[u8] = br#"{"type":"success","id":7,"result":{"vendorExtension":{"clean":true}}}"#; const END_REMOTE_ERROR_RESPONSE: &[u8] = br#"{"type":"error","id":7,"error":"unknown error","message":"remote refused"}"#; +const END_NULL_ID_ERROR_RESPONSE: &[u8] = + br#"{"type":"error","id":null,"error":"unknown error","message":"unattributed"}"#; +const END_EVENT_RESPONSE: &[u8] = + br#"{"type":"event","method":"log.entryAdded","params":{"text":"not a response"}}"#; const END_UNKNOWN_ID_RESPONSE: &[u8] = br#"{"type":"success","id":8,"result":{"vendorExtension":true}}"#; const END_MALFORMED_RESPONSE: &[u8] = br#"{"type":"success","id":7}"#; @@ -200,6 +204,43 @@ fn session_end_remote_error_consumes_only_the_correlated_command() -> Result<(), Ok(()) } +#[test] +fn null_id_error_and_event_do_not_consume_connection_bound_correlation() +-> Result<(), Box> { + for (response, expected) in [ + ( + END_NULL_ID_ERROR_RESPONSE, + WebDriverBiDiCommandCorrelationError::UncorrelatableErrorResponse, + ), + ( + END_EVENT_RESPONSE, + WebDriverBiDiCommandCorrelationError::EventIsNotResponse, + ), + ] { + let (text, mut correlation) = send_end_and_read_response(response)?; + let error = WebDriverBiDiSessionEndResult::parse_and_correlate(&text, &mut correlation) + .err() + .ok_or_else(|| io::Error::other("uncorrelatable message was accepted"))?; + match error { + WebDriverBiDiSessionEndResponseError::Correlation { source } => { + assert_eq!(source, expected); + } + other => { + return Err(io::Error::other(format!( + "uncorrelatable message produced unexpected error: {other}" + )) + .into()); + } + } + assert_eq!( + correlation.outstanding_count(), + 1, + "uncorrelatable message must leave session.end outstanding" + ); + } + Ok(()) +} + #[test] fn malformed_session_end_envelope_fails_before_consuming_correlation() -> Result<(), Box> { From 89e397542060870578c43ba33a894e1854c198c1 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 5 Sep 2026 16:38:48 +0900 Subject: [PATCH 060/105] fix(network): satisfy teardown provenance lint --- .../src/webdriver_bidi_session_teardown.rs | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/crates/originweave-network/src/webdriver_bidi_session_teardown.rs b/crates/originweave-network/src/webdriver_bidi_session_teardown.rs index 7d1295677..d83d947db 100644 --- a/crates/originweave-network/src/webdriver_bidi_session_teardown.rs +++ b/crates/originweave-network/src/webdriver_bidi_session_teardown.rs @@ -101,12 +101,13 @@ impl WebDriverBiDiSessionTeardownAssessment { protocol_ack: WebDriverBiDiSessionEndResult, observations: WebDriverBiDiSessionTeardownObservations, ) -> Result { - if let Some(transport_closure) = observations.transport_closure_observation() { - if transport_closure.connection_generation() != protocol_ack.connection_generation() { - return Err( - WebDriverBiDiSessionTeardownAssessmentError::TransportConnectionMismatch, - ); - } + if observations + .transport_closure_observation() + .is_some_and(|transport_closure| { + transport_closure.connection_generation() != protocol_ack.connection_generation() + }) + { + return Err(WebDriverBiDiSessionTeardownAssessmentError::TransportConnectionMismatch); } Ok(Self { protocol_ack, From 6f6d49f1f09324ccb753ba1fd8cc3aecb0345149 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 5 Sep 2026 16:39:16 +0900 Subject: [PATCH 061/105] fix(network): remove impossible BiDi provenance fallback --- .../src/webdriver_bidi_session_end_response.rs | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/crates/originweave-network/src/webdriver_bidi_session_end_response.rs b/crates/originweave-network/src/webdriver_bidi_session_end_response.rs index 5e113fdd4..23d62e4f5 100644 --- a/crates/originweave-network/src/webdriver_bidi_session_end_response.rs +++ b/crates/originweave-network/src/webdriver_bidi_session_end_response.rs @@ -62,17 +62,10 @@ impl WebDriverBiDiSessionEndResult { })?; match completed.outcome() { - WebDriverBiDiCorrelatedResponseOutcome::Success => { - let connection_generation = completed.connection_generation().ok_or( - WebDriverBiDiSessionEndResponseError::MissingConnectionProvenance { - command_id: completed.command_id(), - }, - )?; - Ok(Self { - command_id: completed.command_id(), - connection_generation, - }) - } + WebDriverBiDiCorrelatedResponseOutcome::Success => Ok(Self { + command_id: completed.command_id(), + connection_generation: message.connection_generation(), + }), WebDriverBiDiCorrelatedResponseOutcome::Error => { Err(WebDriverBiDiSessionEndResponseError::RemoteProtocolError { command_id: completed.command_id(), From ddfac5b6ce35d5793543d2baa4125a3349e3d7ab Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 5 Sep 2026 16:40:11 +0900 Subject: [PATCH 062/105] fix(network): expose generation exhaustion to connection tests --- .../src/webdriver_bidi_connection.rs | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/crates/originweave-network/src/webdriver_bidi_connection.rs b/crates/originweave-network/src/webdriver_bidi_connection.rs index 9a9be9843..c7a941060 100644 --- a/crates/originweave-network/src/webdriver_bidi_connection.rs +++ b/crates/originweave-network/src/webdriver_bidi_connection.rs @@ -29,7 +29,7 @@ fn allocate_connection_generation( counter: &AtomicU64, ) -> Result { counter - .fetch_update(Ordering::Relaxed, Ordering::Relaxed, |current| { + .try_update(Ordering::Relaxed, Ordering::Relaxed, |current| { current.checked_add(1) }) .map(WebDriverBiDiConnectionGeneration) @@ -106,6 +106,14 @@ impl WebDriverBiDiTcpConnectionPlan { fn connect_with( self, connector: &dyn WebDriverBiDiSocketConnector, + ) -> Result { + self.connect_with_generation_counter(connector, &NEXT_CONNECTION_GENERATION) + } + + fn connect_with_generation_counter( + self, + connector: &dyn WebDriverBiDiSocketConnector, + generation_counter: &AtomicU64, ) -> Result { let socket_address = self.target.socket_addr(); let connect_timeout = self.connect_timeout; @@ -130,8 +138,7 @@ impl WebDriverBiDiTcpConnectionPlan { attempt_number, source, })?; - let connection_generation = - allocate_connection_generation(&NEXT_CONNECTION_GENERATION)?; + let connection_generation = allocate_connection_generation(generation_counter)?; return Ok(WebDriverBiDiTcpConnection { stream, verified_peer, From 78ffe1ca05edcfd3469d34bc71a97ea29ea1923d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 5 Sep 2026 16:40:49 +0900 Subject: [PATCH 063/105] test(network): cover connection generation exhaustion --- .../generation_exhaustion_tests.rs | 94 +++++++++++++++++++ 1 file changed, 94 insertions(+) create mode 100644 crates/originweave-network/src/webdriver_bidi_connection/generation_exhaustion_tests.rs diff --git a/crates/originweave-network/src/webdriver_bidi_connection/generation_exhaustion_tests.rs b/crates/originweave-network/src/webdriver_bidi_connection/generation_exhaustion_tests.rs new file mode 100644 index 000000000..e70184cf9 --- /dev/null +++ b/crates/originweave-network/src/webdriver_bidi_connection/generation_exhaustion_tests.rs @@ -0,0 +1,94 @@ +#![allow(clippy::expect_used)] + +use std::{ + cell::{Cell, RefCell}, + io, + net::{SocketAddr, TcpListener, TcpStream}, + sync::atomic::AtomicU64, + time::Duration, +}; + +use originweave_core::{WebDriverBiDiWebSocketConnectTarget, WebDriverBiDiWebSocketEndpoint}; + +use super::{ + WebDriverBiDiSocketConnector, WebDriverBiDiTcpConnectionError, WebDriverBiDiTcpConnectionPlan, +}; + +const SESSION_ID: &str = "01234567-89ab-cdef-0123-456789abcdef"; + +struct VerifiedConnector { + stream: RefCell>, + connect_calls: Cell, + peer_calls: Cell, +} + +impl VerifiedConnector { + fn new(stream: TcpStream) -> Self { + Self { + stream: RefCell::new(Some(stream)), + connect_calls: Cell::new(0), + peer_calls: Cell::new(0), + } + } +} + +impl WebDriverBiDiSocketConnector for VerifiedConnector { + fn connect_timeout( + &self, + _socket_address: &SocketAddr, + _timeout: Duration, + ) -> io::Result { + self.connect_calls.set(self.connect_calls.get() + 1); + self.stream + .borrow_mut() + .take() + .ok_or_else(|| io::Error::other("test stream already consumed")) + } + + fn peer_addr(&self, _stream: &TcpStream) -> io::Result { + self.peer_calls.set(self.peer_calls.get() + 1); + Ok(SocketAddr::from(([127, 0, 0, 1], 9515))) + } +} + +fn loopback_stream() -> TcpStream { + let listener = TcpListener::bind(("127.0.0.1", 0)).expect("bind loopback listener"); + let address = listener + .local_addr() + .expect("read loopback listener address"); + let client = TcpStream::connect(address).expect("connect loopback client"); + let (server, _) = listener.accept().expect("accept loopback client"); + drop(server); + client +} + +fn connect_target() -> WebDriverBiDiWebSocketConnectTarget { + let endpoint = format!("ws://127.0.0.1:9515/session/{SESSION_ID}"); + WebDriverBiDiWebSocketEndpoint::new(&endpoint) + .expect("admit endpoint") + .correlate_session_id(SESSION_ID) + .expect("correlate endpoint") + .into_explicit_connect_target() + .expect("derive explicit connect target") +} + +#[test] +fn verified_connection_fails_closed_when_generation_space_is_exhausted() { + let connector = VerifiedConnector::new(loopback_stream()); + let exhausted_counter = AtomicU64::new(u64::MAX); + let error = WebDriverBiDiTcpConnectionPlan::new( + connect_target(), + Duration::from_millis(250), + 1, + ) + .expect("valid plan") + .connect_with_generation_counter(&connector, &exhausted_counter) + .expect_err("generation exhaustion must fail after exact peer verification"); + + assert!(matches!( + error, + WebDriverBiDiTcpConnectionError::ConnectionGenerationExhausted + )); + assert_eq!(connector.connect_calls.get(), 1); + assert_eq!(connector.peer_calls.get(), 1); +} From 8315eedea8780c24bc66b9e7eef6f3436dec1632 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 5 Sep 2026 16:41:26 +0900 Subject: [PATCH 064/105] test(network): activate generation exhaustion coverage --- crates/originweave-network/src/webdriver_bidi_connection.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/crates/originweave-network/src/webdriver_bidi_connection.rs b/crates/originweave-network/src/webdriver_bidi_connection.rs index c7a941060..f1701a923 100644 --- a/crates/originweave-network/src/webdriver_bidi_connection.rs +++ b/crates/originweave-network/src/webdriver_bidi_connection.rs @@ -13,6 +13,8 @@ mod error; pub use error::WebDriverBiDiTcpConnectionError; +#[cfg(test)] +mod generation_exhaustion_tests; #[cfg(test)] mod tests; From defa1a0f567fb79b83c20ccbe09e229048158f12 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 5 Sep 2026 16:42:20 +0900 Subject: [PATCH 065/105] test(network): restore session.end response formatting --- .../tests/webdriver_bidi_session_end_response.rs | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/crates/originweave-network/tests/webdriver_bidi_session_end_response.rs b/crates/originweave-network/tests/webdriver_bidi_session_end_response.rs index 602577500..effc6bec1 100644 --- a/crates/originweave-network/tests/webdriver_bidi_session_end_response.rs +++ b/crates/originweave-network/tests/webdriver_bidi_session_end_response.rs @@ -140,8 +140,7 @@ fn send_end_and_read_response( } #[test] -fn session_end_success_accepts_extensible_empty_result_and_consumes_exact_correlation() --> Result<(), Box> { +fn session_end_success_consumes_exact_connection_correlation() -> Result<(), Box> { let (text, mut correlation) = send_end_and_read_response(END_SUCCESS_RESPONSE)?; assert_eq!(correlation.outstanding_count(), 1); @@ -152,8 +151,7 @@ fn session_end_success_accepts_extensible_empty_result_and_consumes_exact_correl } #[test] -fn session_end_success_rejects_correlation_without_connection_provenance() --> Result<(), Box> { +fn session_end_rejects_unbound_connection_provenance() -> Result<(), Box> { let (text, _connection_bound_correlation) = send_end_and_read_response(END_SUCCESS_RESPONSE)?; let mut unbound = WebDriverBiDiCommandCorrelation::new(); unbound.register_command_for(7, WebDriverBiDiCommandKind::SessionEnd)?; @@ -205,8 +203,7 @@ fn session_end_remote_error_consumes_only_the_correlated_command() -> Result<(), } #[test] -fn null_id_error_and_event_do_not_consume_connection_bound_correlation() --> Result<(), Box> { +fn null_id_and_event_preserve_bound_correlation() -> Result<(), Box> { for (response, expected) in [ ( END_NULL_ID_ERROR_RESPONSE, @@ -242,8 +239,7 @@ fn null_id_error_and_event_do_not_consume_connection_bound_correlation() } #[test] -fn malformed_session_end_envelope_fails_before_consuming_correlation() -> Result<(), Box> -{ +fn malformed_envelope_preserves_correlation() -> Result<(), Box> { let (text, mut correlation) = send_end_and_read_response(END_MALFORMED_RESPONSE)?; let parsed = WebDriverBiDiSessionEndResult::parse_and_correlate(&text, &mut correlation); let error = match parsed { @@ -267,8 +263,7 @@ fn malformed_session_end_envelope_fails_before_consuming_correlation() -> Result } #[test] -fn unknown_session_end_response_id_does_not_consume_the_outstanding_command() --> Result<(), Box> { +fn unknown_response_id_preserves_outstanding_command() -> Result<(), Box> { let (text, mut correlation) = send_end_and_read_response(END_UNKNOWN_ID_RESPONSE)?; let parsed = WebDriverBiDiSessionEndResult::parse_and_correlate(&text, &mut correlation); let error = match parsed { From 00e007b7aaf6103cac28d88665a867931963e2f8 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 5 Sep 2026 16:43:27 +0900 Subject: [PATCH 066/105] test(network): restore response provenance formatting --- ...ebdriver_bidi_session_end_response_connection_provenance.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/crates/originweave-network/tests/webdriver_bidi_session_end_response_connection_provenance.rs b/crates/originweave-network/tests/webdriver_bidi_session_end_response_connection_provenance.rs index be5dbade5..bcbc53ae0 100644 --- a/crates/originweave-network/tests/webdriver_bidi_session_end_response_connection_provenance.rs +++ b/crates/originweave-network/tests/webdriver_bidi_session_end_response_connection_provenance.rs @@ -98,8 +98,7 @@ fn read_one_connection_bound_text( } #[test] -fn response_received_on_reconnected_transport_cannot_consume_prior_connection_command() --> Result<(), Box> { +fn reconnected_response_cannot_consume_prior_connection_command() -> Result<(), Box> { let listener = TcpListener::bind(("127.0.0.1", 0))?; let local_addr = listener.local_addr()?; let server = thread::spawn(move || -> io::Result<()> { From 2c8e9e29ac1560afe903d3690eb63c45e625d205 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 5 Sep 2026 16:44:11 +0900 Subject: [PATCH 067/105] test(network): restore teardown assessment formatting --- .../tests/webdriver_bidi_session_teardown_assessment.rs | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/crates/originweave-network/tests/webdriver_bidi_session_teardown_assessment.rs b/crates/originweave-network/tests/webdriver_bidi_session_teardown_assessment.rs index d17dc159a..90c17b6c0 100644 --- a/crates/originweave-network/tests/webdriver_bidi_session_teardown_assessment.rs +++ b/crates/originweave-network/tests/webdriver_bidi_session_teardown_assessment.rs @@ -212,8 +212,7 @@ fn missing_typed_transport_observation_keeps_teardown_pending() -> Result<(), Bo } #[test] -fn typed_transport_closure_cannot_complete_teardown_without_process_and_profile_evidence() --> Result<(), Box> { +fn typed_closure_still_requires_process_and_profile_evidence() -> Result<(), Box> { let (acknowledged, established) = correlated_session_end_ack_and_transport()?; let transport_closure = observed_transport_closure(established)?; let assessment = WebDriverBiDiSessionTeardownAssessment::from_protocol_ack( @@ -263,8 +262,7 @@ fn assert_cross_connection_closure_rejected( } #[test] -fn reconnected_same_session_and_endpoint_cannot_supply_closure_for_prior_transport() --> Result<(), Box> { +fn same_session_reconnect_cannot_supply_prior_closure() -> Result<(), Box> { let listener = TcpListener::bind(("127.0.0.1", 0))?; let (acknowledged_a, established_a) = correlated_session_end_ack_and_transport_on(&listener, SESSION_ID)?; @@ -275,8 +273,7 @@ fn reconnected_same_session_and_endpoint_cannot_supply_closure_for_prior_transpo } #[test] -fn another_session_cannot_supply_closure_for_the_acknowledged_transport() --> Result<(), Box> { +fn another_session_cannot_supply_acknowledged_transport_closure() -> Result<(), Box> { let (acknowledged_a, established_a) = correlated_session_end_ack_and_transport_for(SESSION_ID)?; drop(established_a); let (_acknowledged_b, established_b) = From 64b88e6b5d990fdb7c952ac058f451e242480092 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 5 Sep 2026 16:44:47 +0900 Subject: [PATCH 068/105] docs(network): record BiDi provenance quality-gate repair --- ...-received-response-connection-provenance.md | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/docs/doctoring/webdriver-bidi-received-response-connection-provenance.md b/docs/doctoring/webdriver-bidi-received-response-connection-provenance.md index e565e343b..d5d0f7e04 100644 --- a/docs/doctoring/webdriver-bidi-received-response-connection-provenance.md +++ b/docs/doctoring/webdriver-bidi-received-response-connection-provenance.md @@ -10,7 +10,7 @@ The test-first commit `0a94765e0c631a043febec0973fd043304877537` reproduces this ## Constraints and rejected alternatives -WebDriver BiDi uses the command `id` only so the local end can identify a command response; commands may finish out of order, and the id is opaque to the remote end. The current specification's command-processing algorithm sends the resulting WebSocket message over the same `connection` on which the command was received. OriginWeave therefore needs local evidence of the receiving connection in addition to the response id. +WebDriver BiDi uses the command `id` only so the local end can identify a command response; commands may finish out of order, and the id is opaque to the remote end. The 3 September 2026 W3C Working Draft's command-processing algorithms retain the WebSocket connection as an explicit input/output boundary. OriginWeave therefore needs local evidence of the receiving connection in addition to the response id. Passing a caller-supplied connection generation into response parsing was rejected because it would turn provenance into forgeable metadata. Passing a bare assembled text message plus a separately supplied established connection was also rejected because callers could accidentally pair a message from B with connection A. Adding a connection id to JSON was rejected because it would invent protocol data not defined by WebDriver BiDi. @@ -18,13 +18,21 @@ Passing a caller-supplied connection generation into response parsing was reject `WebDriverBiDiWebSocketMessageReader` consumes one established WebSocket and owns its message assembler. Every text fragment admitted by that reader is therefore read from the same non-cloneable verified transport. `Pending` and interleaved control outcomes retain the reader so fragmented state cannot be moved onto another connection. Once a text message is complete, the reader emits `WebDriverBiDiReceivedTextMessage` carrying the private process-local connection generation and returns the established transport separately because no partial fragments remain. -`session.end` response admission now requires that received-message type. Correlation validates command kind, stored connection provenance, and received connection generation before removing the outstanding id. Missing provenance and a different received connection both fail closed without consuming the command. Protocol ACK remains distinct from transport closure, Chromium process exit, profile deletion, and task completion. +`session.end` response admission requires that received-message type. Correlation validates command kind, stored connection provenance, and received connection generation before removing the outstanding id. Missing provenance and a different received connection both fail closed without consuming the command. Protocol ACK remains distinct from transport closure, Chromium process exit, profile deletion, and task completion. This placement also preserves RFC 6455 fragmentation semantics: a message may span multiple frames, control frames may appear between fragments, recipients must support fragmented and unfragmented messages, and message fragments are delivered in order on the WebSocket connection. The reader therefore binds the assembled-message transaction to one connection rather than treating individual application payloads as free-standing evidence. +## Quality-gate follow-up + +A complete local verification snapshot at `e7bfec4488b7cb4776df7b546cacb46c8c9eb13e` established a second RED lane: the behavioral repair passed its focused connection-provenance tests, but rustfmt and strict Clippy were not clean and production coverage still missed connection-bound event/null-id errors, connection-generation exhaustion propagation, and a post-correlation provenance fallback that could not be reached after successful connection-bound correlation. Those failures are quality-gate evidence, not hosted GREEN. + +The follow-up keeps the security invariant and removes the gate-specific causes rather than excluding them. `feba41f13fb39e83fc8b377b8a55bd7c27348f9d` exercises event and null-id envelopes through the real `session.end` response path without consuming the outstanding command. The teardown provenance comparison is expressed without the nested `if` rejected by strict Clippy. Successful connection-bound correlation now carries the already-validated received-message generation directly into `WebDriverBiDiSessionEndResult`, eliminating the impossible optional-provenance fallback instead of excluding it from coverage. Connection establishment accepts a private generation-counter seam so the exhaustion error can be exercised after exact peer verification, and the allocator uses `AtomicU64::try_update`; Rust documents `try_update` as available since 1.95.0, which remains compatible with OriginWeave's Rust 1.97.1 baseline. The added exhaustion regression uses a real loopback TCP stream plus a verified peer result and requires `ConnectionGenerationExhausted` after one connect and one peer-inspection call. + +Formatting-only test-name repairs preserve the realistic transport scenarios while restoring conventional rustfmt layout. Exact hosted CI/coverage/security results still belong to the live PR head and must be read from GitHub; predecessor results do not transfer. + ## Evidence and remaining risk -The repair adds a realistic two-connection regression for a foreign `session.end` success response and focused reader coverage for fragmented text, an interleaved control frame, malformed server framing, and binary-message rejection. Exact hosted CI/coverage/security results belong to the live PR head and must be read from GitHub; predecessor results do not transfer. +The repair includes a realistic two-connection regression for a foreign `session.end` success response and focused reader coverage for fragmented text, an interleaved control frame, malformed server framing, binary-message rejection, event/null-id correlation, and connection-generation exhaustion after verified peer admission. The original inline response-substitution and closure-substitution findings are resolved by the current source boundary, but exact-head CI and security workflows remain authoritative before any integration claim. The current scope deliberately does not infer browser-process ownership from the WebSocket connection and does not make protocol success an operational teardown post-condition. Process-exit and profile-removal evidence remain unavailable until their canonical runtime owners provide non-forgeable contracts. @@ -32,4 +40,6 @@ The current scope deliberately does not infer browser-process ownership from the Fette, I., & Melnikov, A. (2011). *The WebSocket Protocol* (RFC 6455). Internet Engineering Task Force. https://www.rfc-editor.org/rfc/rfc6455 -W3C Browser Testing and Tools Working Group. (2026). *WebDriver BiDi*. World Wide Web Consortium. https://w3c.github.io/webdriver-bidi/ +The Rust Project Developers. (n.d.). *AtomicU64 in std::sync::atomic*. Rust standard library documentation. Retrieved September 5, 2026, from https://doc.rust-lang.org/std/sync/atomic/struct.AtomicU64.html + +W3C Browser Testing and Tools Working Group. (2026, September 3). *WebDriver BiDi* (W3C Working Draft). World Wide Web Consortium. https://www.w3.org/TR/2026/WD-webdriver-bidi-20260903/ From 63cbca0a98cf9496af981819d98029e656fc4342 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 5 Sep 2026 16:46:29 +0900 Subject: [PATCH 069/105] docs(network): align provenance note with Rust MSRV --- .../webdriver-bidi-received-response-connection-provenance.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/doctoring/webdriver-bidi-received-response-connection-provenance.md b/docs/doctoring/webdriver-bidi-received-response-connection-provenance.md index d5d0f7e04..87bf3bf54 100644 --- a/docs/doctoring/webdriver-bidi-received-response-connection-provenance.md +++ b/docs/doctoring/webdriver-bidi-received-response-connection-provenance.md @@ -26,7 +26,7 @@ This placement also preserves RFC 6455 fragmentation semantics: a message may sp A complete local verification snapshot at `e7bfec4488b7cb4776df7b546cacb46c8c9eb13e` established a second RED lane: the behavioral repair passed its focused connection-provenance tests, but rustfmt and strict Clippy were not clean and production coverage still missed connection-bound event/null-id errors, connection-generation exhaustion propagation, and a post-correlation provenance fallback that could not be reached after successful connection-bound correlation. Those failures are quality-gate evidence, not hosted GREEN. -The follow-up keeps the security invariant and removes the gate-specific causes rather than excluding them. `feba41f13fb39e83fc8b377b8a55bd7c27348f9d` exercises event and null-id envelopes through the real `session.end` response path without consuming the outstanding command. The teardown provenance comparison is expressed without the nested `if` rejected by strict Clippy. Successful connection-bound correlation now carries the already-validated received-message generation directly into `WebDriverBiDiSessionEndResult`, eliminating the impossible optional-provenance fallback instead of excluding it from coverage. Connection establishment accepts a private generation-counter seam so the exhaustion error can be exercised after exact peer verification, and the allocator uses `AtomicU64::try_update`; Rust documents `try_update` as available since 1.95.0, which remains compatible with OriginWeave's Rust 1.97.1 baseline. The added exhaustion regression uses a real loopback TCP stream plus a verified peer result and requires `ConnectionGenerationExhausted` after one connect and one peer-inspection call. +The follow-up keeps the security invariant and removes the gate-specific causes rather than excluding them. `feba41f13fb39e83fc8b377b8a55bd7c27348f9d` exercises event and null-id envelopes through the real `session.end` response path without consuming the outstanding command. The teardown provenance comparison is expressed without the nested `if` rejected by strict Clippy. Successful connection-bound correlation now carries the already-validated received-message generation directly into `WebDriverBiDiSessionEndResult`, eliminating the impossible optional-provenance fallback instead of excluding it from coverage. Connection establishment accepts a private generation-counter seam so the exhaustion error can be exercised after exact peer verification, and the allocator uses `AtomicU64::try_update`; Rust documents `try_update` as available since 1.95.0, which remains compatible with OriginWeave's declared Rust 1.97 MSRV. The added exhaustion regression uses a real loopback TCP stream plus a verified peer result and requires `ConnectionGenerationExhausted` after one connect and one peer-inspection call. Formatting-only test-name repairs preserve the realistic transport scenarios while restoring conventional rustfmt layout. Exact hosted CI/coverage/security results still belong to the live PR head and must be read from GitHub; predecessor results do not transfer. From ebac126d1632c94775c2454423575275eec45def Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 5 Sep 2026 18:06:15 +0900 Subject: [PATCH 070/105] fix(network): remove unused correlation accessor --- CHANGELOG.md | 4 +++ .../src/webdriver_bidi_command_correlation.rs | 30 +++++------------- .../src/webdriver_bidi_connection/error.rs | 5 +-- .../generation_exhaustion_tests.rs | 13 +++----- .../src/webdriver_bidi_received_message.rs | 13 ++++---- .../src/webdriver_bidi_session_end_command.rs | 4 +-- .../webdriver_bidi_session_end_response.rs | 31 ++++++++----------- ...driver_bidi_websocket_transport_closure.rs | 15 +++------ .../webdriver_bidi_session_end_response.rs | 10 +++--- ...sion_end_response_connection_provenance.rs | 6 ++-- ...received-response-connection-provenance.md | 4 ++- 11 files changed, 55 insertions(+), 80 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c2792da0c..b7795d237 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,10 @@ All notable changes to OriginWeave are documented in this file. The format follo ## [Unreleased] +### Changed + +- Removed an unused private correlated-response accessor while retaining connection-generation validation at the receiving-message boundary, and corrected the Rust `AtomicU64` standard-library reference to its canonical type-alias page. + ### Added - Typed outbound WebDriver BiDi `session.end` over the bounded client WebSocket stream: it serializes only the standards-defined method with empty params, rejects invalid frame deadlines before correlation registration, retires only the just-registered id when frame preflight proves no command bytes were emitted, preserves exact command-kind correlation across ambiguous writes, and does not treat frame-write success as proof that the browser session ended. diff --git a/crates/originweave-network/src/webdriver_bidi_command_correlation.rs b/crates/originweave-network/src/webdriver_bidi_command_correlation.rs index d42bae2e5..2b66551b0 100644 --- a/crates/originweave-network/src/webdriver_bidi_command_correlation.rs +++ b/crates/originweave-network/src/webdriver_bidi_command_correlation.rs @@ -1,8 +1,8 @@ use std::{collections::BTreeMap, error::Error, fmt}; use crate::{ - webdriver_bidi_connection::WebDriverBiDiConnectionGeneration, MAX_WEBDRIVER_BIDI_JS_UINT, - WebDriverBiDiJsonEnvelope, WebDriverBiDiJsonEnvelopeRouting, + MAX_WEBDRIVER_BIDI_JS_UINT, WebDriverBiDiJsonEnvelope, WebDriverBiDiJsonEnvelopeRouting, + webdriver_bidi_connection::WebDriverBiDiConnectionGeneration, }; /// Maximum number of local WebDriver BiDi commands retained as outstanding at once. @@ -66,12 +66,6 @@ impl WebDriverBiDiCorrelatedResponse { pub const fn outcome(&self) -> WebDriverBiDiCorrelatedResponseOutcome { self.outcome } - - pub(crate) const fn connection_generation( - &self, - ) -> Option { - self.connection_generation - } } /// Fail-closed command-correlation failures at the local WebDriver BiDi response boundary. @@ -192,11 +186,7 @@ impl WebDriverBiDiCommandCorrelation { command_kind: WebDriverBiDiCommandKind, connection_generation: WebDriverBiDiConnectionGeneration, ) -> Result<(), WebDriverBiDiCommandCorrelationError> { - self.register( - command_id, - command_kind, - Some(connection_generation), - ) + self.register(command_id, command_kind, Some(connection_generation)) } fn register( @@ -345,14 +335,12 @@ impl WebDriverBiDiCommandCorrelation { ) -> Result { let outstanding = self.require_command_kind(command_id, expected_kind)?; let expected_connection_generation = outstanding.connection_generation.ok_or( - WebDriverBiDiCommandCorrelationError::CommandConnectionProvenanceMissing { - command_id, - }, + WebDriverBiDiCommandCorrelationError::CommandConnectionProvenanceMissing { command_id }, )?; if expected_connection_generation != received_connection_generation { - return Err(WebDriverBiDiCommandCorrelationError::ResponseConnectionMismatch { - command_id, - }); + return Err( + WebDriverBiDiCommandCorrelationError::ResponseConnectionMismatch { command_id }, + ); } let _removed = self.outstanding.remove(&command_id); Ok(WebDriverBiDiCorrelatedResponse { @@ -400,9 +388,7 @@ mod tests { "WebDriver BiDi outstanding command lacks connection provenance", ), ( - WebDriverBiDiCommandCorrelationError::ResponseConnectionMismatch { - command_id: 7, - }, + WebDriverBiDiCommandCorrelationError::ResponseConnectionMismatch { command_id: 7 }, "WebDriver BiDi response arrived on a different connection", ), ( diff --git a/crates/originweave-network/src/webdriver_bidi_connection/error.rs b/crates/originweave-network/src/webdriver_bidi_connection/error.rs index ca8aa41b6..75e574acf 100644 --- a/crates/originweave-network/src/webdriver_bidi_connection/error.rs +++ b/crates/originweave-network/src/webdriver_bidi_connection/error.rs @@ -92,8 +92,9 @@ impl fmt::Display for WebDriverBiDiTcpConnectionError { formatter, "WebDriver BiDi connection attempt count {attempt_count} is outside 1..={maximum_attempts}", ), - Self::ConnectionGenerationExhausted => formatter - .write_str("WebDriver BiDi connection generation space is exhausted"), + Self::ConnectionGenerationExhausted => { + formatter.write_str("WebDriver BiDi connection generation space is exhausted") + } Self::ConnectionTimedOut { socket_address, attempt_count, diff --git a/crates/originweave-network/src/webdriver_bidi_connection/generation_exhaustion_tests.rs b/crates/originweave-network/src/webdriver_bidi_connection/generation_exhaustion_tests.rs index e70184cf9..071216ad5 100644 --- a/crates/originweave-network/src/webdriver_bidi_connection/generation_exhaustion_tests.rs +++ b/crates/originweave-network/src/webdriver_bidi_connection/generation_exhaustion_tests.rs @@ -76,14 +76,11 @@ fn connect_target() -> WebDriverBiDiWebSocketConnectTarget { fn verified_connection_fails_closed_when_generation_space_is_exhausted() { let connector = VerifiedConnector::new(loopback_stream()); let exhausted_counter = AtomicU64::new(u64::MAX); - let error = WebDriverBiDiTcpConnectionPlan::new( - connect_target(), - Duration::from_millis(250), - 1, - ) - .expect("valid plan") - .connect_with_generation_counter(&connector, &exhausted_counter) - .expect_err("generation exhaustion must fail after exact peer verification"); + let error = + WebDriverBiDiTcpConnectionPlan::new(connect_target(), Duration::from_millis(250), 1) + .expect("valid plan") + .connect_with_generation_counter(&connector, &exhausted_counter) + .expect_err("generation exhaustion must fail after exact peer verification"); assert!(matches!( error, diff --git a/crates/originweave-network/src/webdriver_bidi_received_message.rs b/crates/originweave-network/src/webdriver_bidi_received_message.rs index 671810942..9b9422c93 100644 --- a/crates/originweave-network/src/webdriver_bidi_received_message.rs +++ b/crates/originweave-network/src/webdriver_bidi_received_message.rs @@ -1,11 +1,11 @@ use std::{error::Error, fmt, time::Duration}; use crate::{ - webdriver_bidi_connection::WebDriverBiDiConnectionGeneration, WebDriverBiDiWebSocketControlMessage, WebDriverBiDiWebSocketEstablished, WebDriverBiDiWebSocketFrameError, WebDriverBiDiWebSocketMessageAssembler, WebDriverBiDiWebSocketMessageAssembly, WebDriverBiDiWebSocketMessageError, WebDriverBiDiWebSocketTextMessage, + webdriver_bidi_connection::WebDriverBiDiConnectionGeneration, }; /// One complete WebDriver BiDi text message bound to the exact verified connection that read it. @@ -174,12 +174,11 @@ pub enum WebDriverBiDiConnectionMessageReadError { impl fmt::Display for WebDriverBiDiConnectionMessageReadError { fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { match self { - Self::Frame { .. } => formatter.write_str( - "connection-bound WebDriver BiDi WebSocket frame read failed", - ), - Self::Message { .. } => formatter.write_str( - "connection-bound WebDriver BiDi WebSocket message assembly failed", - ), + Self::Frame { .. } => { + formatter.write_str("connection-bound WebDriver BiDi WebSocket frame read failed") + } + Self::Message { .. } => formatter + .write_str("connection-bound WebDriver BiDi WebSocket message assembly failed"), } } } diff --git a/crates/originweave-network/src/webdriver_bidi_session_end_command.rs b/crates/originweave-network/src/webdriver_bidi_session_end_command.rs index d8a07b0ed..82dc95cf7 100644 --- a/crates/originweave-network/src/webdriver_bidi_session_end_command.rs +++ b/crates/originweave-network/src/webdriver_bidi_session_end_command.rs @@ -62,9 +62,7 @@ impl WebDriverBiDiSessionEndCommand { }, }); } - let connection_generation = established - .transport_evidence() - .connection_generation(); + let connection_generation = established.transport_evidence().connection_generation(); correlation .register_command_for_connection( self.command_id, diff --git a/crates/originweave-network/src/webdriver_bidi_session_end_response.rs b/crates/originweave-network/src/webdriver_bidi_session_end_response.rs index 23d62e4f5..003bee300 100644 --- a/crates/originweave-network/src/webdriver_bidi_session_end_response.rs +++ b/crates/originweave-network/src/webdriver_bidi_session_end_response.rs @@ -1,10 +1,10 @@ use std::{error::Error, fmt}; use crate::{ - webdriver_bidi_connection::WebDriverBiDiConnectionGeneration, WebDriverBiDiCommandCorrelation, - WebDriverBiDiCommandCorrelationError, WebDriverBiDiCommandKind, - WebDriverBiDiCorrelatedResponseOutcome, WebDriverBiDiJsonEnvelope, + WebDriverBiDiCommandCorrelation, WebDriverBiDiCommandCorrelationError, + WebDriverBiDiCommandKind, WebDriverBiDiCorrelatedResponseOutcome, WebDriverBiDiJsonEnvelope, WebDriverBiDiJsonEnvelopeError, WebDriverBiDiReceivedTextMessage, + webdriver_bidi_connection::WebDriverBiDiConnectionGeneration, }; /// Typed protocol acknowledgment for one correlated WebDriver BiDi `session.end` command. @@ -50,13 +50,11 @@ impl WebDriverBiDiSessionEndResult { .map_err(|source| match source { WebDriverBiDiCommandCorrelationError::CommandConnectionProvenanceMissing { command_id, - } => WebDriverBiDiSessionEndResponseError::MissingConnectionProvenance { - command_id, - }, + } => { + WebDriverBiDiSessionEndResponseError::MissingConnectionProvenance { command_id } + } WebDriverBiDiCommandCorrelationError::ResponseConnectionMismatch { command_id } => { - WebDriverBiDiSessionEndResponseError::TransportConnectionMismatch { - command_id, - } + WebDriverBiDiSessionEndResponseError::TransportConnectionMismatch { command_id } } source => WebDriverBiDiSessionEndResponseError::Correlation { source }, })?; @@ -126,9 +124,8 @@ impl fmt::Display for WebDriverBiDiSessionEndResponseError { } Self::MissingConnectionProvenance { .. } => formatter .write_str("WebDriver BiDi session.end response lacks connection provenance"), - Self::TransportConnectionMismatch { .. } => formatter.write_str( - "WebDriver BiDi session.end response arrived on a different connection", - ), + Self::TransportConnectionMismatch { .. } => formatter + .write_str("WebDriver BiDi session.end response arrived on a different connection"), Self::RemoteProtocolError { .. } => { formatter.write_str("WebDriver BiDi session.end returned a protocol error") } @@ -172,18 +169,16 @@ mod tests { ); assert!(correlation.source().is_some()); - let missing = WebDriverBiDiSessionEndResponseError::MissingConnectionProvenance { - command_id: 7, - }; + let missing = + WebDriverBiDiSessionEndResponseError::MissingConnectionProvenance { command_id: 7 }; assert_eq!( missing.to_string(), "WebDriver BiDi session.end response lacks connection provenance" ); assert!(missing.source().is_none()); - let mismatch = WebDriverBiDiSessionEndResponseError::TransportConnectionMismatch { - command_id: 7, - }; + let mismatch = + WebDriverBiDiSessionEndResponseError::TransportConnectionMismatch { command_id: 7 }; assert_eq!( mismatch.to_string(), "WebDriver BiDi session.end response arrived on a different connection" diff --git a/crates/originweave-network/src/webdriver_bidi_websocket_transport_closure.rs b/crates/originweave-network/src/webdriver_bidi_websocket_transport_closure.rs index 2da3ce4e7..e3298c304 100644 --- a/crates/originweave-network/src/webdriver_bidi_websocket_transport_closure.rs +++ b/crates/originweave-network/src/webdriver_bidi_websocket_transport_closure.rs @@ -1,8 +1,8 @@ use std::{error::Error, fmt, time::Duration}; use crate::{ - webdriver_bidi_connection::WebDriverBiDiConnectionGeneration, WebDriverBiDiWebSocketEstablished, WebDriverBiDiWebSocketFrameError, + webdriver_bidi_connection::WebDriverBiDiConnectionGeneration, }; /// Bounded transport-closure condition observed on one consumed WebDriver BiDi WebSocket. @@ -47,9 +47,7 @@ impl WebDriverBiDiWebSocketTransportClosureObservation { established: WebDriverBiDiWebSocketEstablished, frame_timeout: Duration, ) -> Result { - let connection_generation = established - .transport_evidence() - .connection_generation(); + let connection_generation = established.transport_evidence().connection_generation(); Self::observe_frame(established, frame_timeout, true, connection_generation) } @@ -60,12 +58,9 @@ impl WebDriverBiDiWebSocketTransportClosureObservation { connection_generation: WebDriverBiDiConnectionGeneration, ) -> Result { match established.read_frame(frame_timeout) { - Ok((established, frame)) if frame.opcode() == 0xa && allow_pong => Self::observe_frame( - established, - frame_timeout, - false, - connection_generation, - ), + Ok((established, frame)) if frame.opcode() == 0xa && allow_pong => { + Self::observe_frame(established, frame_timeout, false, connection_generation) + } Ok((established, frame)) => { if frame.opcode() != 0x8 { return Err( diff --git a/crates/originweave-network/tests/webdriver_bidi_session_end_response.rs b/crates/originweave-network/tests/webdriver_bidi_session_end_response.rs index effc6bec1..2d243d33b 100644 --- a/crates/originweave-network/tests/webdriver_bidi_session_end_response.rs +++ b/crates/originweave-network/tests/webdriver_bidi_session_end_response.rs @@ -8,12 +8,12 @@ use std::{ use originweave_core::WebDriverBiDiWebSocketEndpoint; use originweave_network::{ - WebDriverBiDiCommandCorrelation, WebDriverBiDiCommandCorrelationError, WebDriverBiDiCommandKind, - WebDriverBiDiConnectionMessageRead, WebDriverBiDiReceivedTextMessage, + WebDriverBiDiCommandCorrelation, WebDriverBiDiCommandCorrelationError, + WebDriverBiDiCommandKind, WebDriverBiDiConnectionMessageRead, WebDriverBiDiReceivedTextMessage, WebDriverBiDiSessionEndCommand, WebDriverBiDiSessionEndResponseError, - WebDriverBiDiSessionEndResult, WebDriverBiDiTcpConnectionPlan, - WebDriverBiDiWebSocketClientKey, WebDriverBiDiWebSocketHandshakePlan, - WebDriverBiDiWebSocketMaskKey, WebDriverBiDiWebSocketMessageReader, + WebDriverBiDiSessionEndResult, WebDriverBiDiTcpConnectionPlan, WebDriverBiDiWebSocketClientKey, + WebDriverBiDiWebSocketHandshakePlan, WebDriverBiDiWebSocketMaskKey, + WebDriverBiDiWebSocketMessageReader, }; const SESSION_ID: &str = "01234567-89ab-cdef-0123-456789abcdef"; diff --git a/crates/originweave-network/tests/webdriver_bidi_session_end_response_connection_provenance.rs b/crates/originweave-network/tests/webdriver_bidi_session_end_response_connection_provenance.rs index bcbc53ae0..2f3c69fa0 100644 --- a/crates/originweave-network/tests/webdriver_bidi_session_end_response_connection_provenance.rs +++ b/crates/originweave-network/tests/webdriver_bidi_session_end_response_connection_provenance.rs @@ -133,10 +133,8 @@ fn reconnected_response_cannot_consume_prior_connection_command() -> Result<(), let second = establish(local_addr)?; let foreign_response = read_one_connection_bound_text(second)?; - let parsed = WebDriverBiDiSessionEndResult::parse_and_correlate( - &foreign_response, - &mut correlation, - ); + let parsed = + WebDriverBiDiSessionEndResult::parse_and_correlate(&foreign_response, &mut correlation); let error = parsed .err() .ok_or_else(|| io::Error::other("foreign response acknowledged prior connection"))?; diff --git a/docs/doctoring/webdriver-bidi-received-response-connection-provenance.md b/docs/doctoring/webdriver-bidi-received-response-connection-provenance.md index 87bf3bf54..26739b4a9 100644 --- a/docs/doctoring/webdriver-bidi-received-response-connection-provenance.md +++ b/docs/doctoring/webdriver-bidi-received-response-connection-provenance.md @@ -30,6 +30,8 @@ The follow-up keeps the security invariant and removes the gate-specific causes Formatting-only test-name repairs preserve the realistic transport scenarios while restoring conventional rustfmt layout. Exact hosted CI/coverage/security results still belong to the live PR head and must be read from GitHub; predecessor results do not transfer. +The `63cbca0a98cf9496af981819d98029e656fc4342` follow-up removed the unused private correlated-response generation accessor, not the stored provenance or its validation. Rust 1.97.1 rustfmt, strict Clippy, workspace tests, rustdoc with `-D warnings`, and all 141 Python contracts then passed locally. The existing production coverage checker reported 1082/1082 functions, 11025/11025 lines, 14071/14071 regions, and 1202/1202 branches. However, pinned `cargo-llvm-cov 0.8.6` emitted `warning: --branch option is unstable and it may be changed in the future`. These numerical coverage results are not warning-free acceptance evidence. Keep the warning visible and the gate outstanding; do not suppress it, remove branch measurement, or change the denominator to manufacture a clean result. + ## Evidence and remaining risk The repair includes a realistic two-connection regression for a foreign `session.end` success response and focused reader coverage for fragmented text, an interleaved control frame, malformed server framing, binary-message rejection, event/null-id correlation, and connection-generation exhaustion after verified peer admission. The original inline response-substitution and closure-substitution findings are resolved by the current source boundary, but exact-head CI and security workflows remain authoritative before any integration claim. @@ -40,6 +42,6 @@ The current scope deliberately does not infer browser-process ownership from the Fette, I., & Melnikov, A. (2011). *The WebSocket Protocol* (RFC 6455). Internet Engineering Task Force. https://www.rfc-editor.org/rfc/rfc6455 -The Rust Project Developers. (n.d.). *AtomicU64 in std::sync::atomic*. Rust standard library documentation. Retrieved September 5, 2026, from https://doc.rust-lang.org/std/sync/atomic/struct.AtomicU64.html +The Rust Project Developers. (n.d.). *AtomicU64 in std::sync::atomic*. Rust standard library documentation. Retrieved September 5, 2026, from https://doc.rust-lang.org/std/sync/atomic/type.AtomicU64.html W3C Browser Testing and Tools Working Group. (2026, September 3). *WebDriver BiDi* (W3C Working Draft). World Wide Web Consortium. https://www.w3.org/TR/2026/WD-webdriver-bidi-20260903/ From 6865faa8185c5fe6a8b6aaba9535d774575b0061 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 6 Sep 2026 10:09:38 +0900 Subject: [PATCH 071/105] chore(stack): bind repaired teardown successor to response base Retarget PR #255 directly to the verified #252 session.end response prerequisite so the unsafe caller-supplied teardown assessment in #253 is not a protected-main prerequisite. The tree is intentionally unchanged: #255 already contains #253/#254 valid deltas and the connection-provenance repair. This marker binds fresh exact-head checks to the repaired PR topology without force-pushing or discarding ancestor history. Signed-off-by: Seongho Bae From 32b5615f8f7c41eec8f7e552b48d67b4a713039a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 7 Sep 2026 14:46:23 +0900 Subject: [PATCH 072/105] test(network): reject replacement status replies in teardown stack Co-Authored-By: OpenAI Codex Signed-off-by: Seongho Bae --- .../webdriver_bidi_session_status_response.rs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/crates/originweave-network/tests/webdriver_bidi_session_status_response.rs b/crates/originweave-network/tests/webdriver_bidi_session_status_response.rs index 347dc5fec..4d1577e77 100644 --- a/crates/originweave-network/tests/webdriver_bidi_session_status_response.rs +++ b/crates/originweave-network/tests/webdriver_bidi_session_status_response.rs @@ -25,6 +25,22 @@ const STATUS_RESPONSE_MISSING_READY: &[u8] = br#"{"type":"success","id":7,"result":{"message":"capacity available"}}"#; const STATUS_RESPONSE_EMPTY_RESULT: &[u8] = br#"{"type":"success","id":7,"result":{}}"#; +#[test] +fn teardown_stack_rejects_replacement_status_reply_and_preserves_original_request() +-> Result<(), Box> { + let (original, mut pending) = send_status_and_read_response(STATUS_RESPONSE)?; + let (replacement, _) = send_status_and_read_response(STATUS_RESPONSE)?; + assert!( + WebDriverBiDiSessionStatusResult::parse_and_correlate(&replacement, &mut pending).is_err(), + "a replacement status reply must not complete the original request" + ); + assert_eq!(pending.outstanding_count(), 1); + let result = WebDriverBiDiSessionStatusResult::parse_and_correlate(&original, &mut pending)?; + assert_eq!(result.command_id(), 7); + assert_eq!(pending.outstanding_count(), 0); + Ok(()) +} + fn read_opening_request(stream: &mut TcpStream) -> io::Result<()> { stream.set_read_timeout(Some(Duration::from_secs(2)))?; let mut request = Vec::new(); From b97f57e58a4f560e0f63c0d183c693d6204fd54b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 8 Sep 2026 03:06:32 +0900 Subject: [PATCH 073/105] test(network): reject peer-close-only transport evidence --- ...webdriver_bidi_transport_close_evidence.rs | 58 ++++++++++++++++++- 1 file changed, 56 insertions(+), 2 deletions(-) diff --git a/crates/originweave-network/tests/webdriver_bidi_transport_close_evidence.rs b/crates/originweave-network/tests/webdriver_bidi_transport_close_evidence.rs index 073ecb790..b6327a22e 100644 --- a/crates/originweave-network/tests/webdriver_bidi_transport_close_evidence.rs +++ b/crates/originweave-network/tests/webdriver_bidi_transport_close_evidence.rs @@ -9,8 +9,9 @@ use std::{ use originweave_core::WebDriverBiDiWebSocketEndpoint; use originweave_network::{ WebDriverBiDiTcpConnectionPlan, WebDriverBiDiWebSocketClientKey, - WebDriverBiDiWebSocketHandshakePlan, WebDriverBiDiWebSocketTransportClosureError, - WebDriverBiDiWebSocketTransportClosureKind, WebDriverBiDiWebSocketTransportClosureObservation, + WebDriverBiDiWebSocketFrameError, WebDriverBiDiWebSocketHandshakePlan, + WebDriverBiDiWebSocketTransportClosureError, WebDriverBiDiWebSocketTransportClosureKind, + WebDriverBiDiWebSocketTransportClosureObservation, }; const SESSION_ID: &str = "01234567-89ab-cdef-0123-456789abcdef"; @@ -67,6 +68,31 @@ fn established_with_server_frame( Ok((established, server)) } +fn established_with_held_open_peer_close() -> Result> { + let listener = TcpListener::bind(("127.0.0.1", 0))?; + let local_addr = listener.local_addr()?; + let server = thread::spawn(move || -> io::Result<()> { + let (mut stream, _) = listener.accept()?; + read_opening_request(&mut stream)?; + stream.write_all(OPENING_RESPONSE)?; + stream.write_all(&[0x88, 0x02, 0x03, 0xe8])?; + thread::sleep(Duration::from_millis(250)); + Ok(()) + }); + + let endpoint = format!("ws://{local_addr}/session/{SESSION_ID}"); + let target = WebDriverBiDiWebSocketEndpoint::new(&endpoint)? + .correlate_session_id(SESSION_ID)? + .into_explicit_connect_target()?; + let connection = + WebDriverBiDiTcpConnectionPlan::new(target, Duration::from_secs(1), 1)?.connect()?; + let key = WebDriverBiDiWebSocketClientKey::new(RFC6455_SAMPLE_KEY)?; + let established = WebDriverBiDiWebSocketHandshakePlan::new(connection, key)? + .write_opening_request(Duration::from_millis(500))? + .read_opening_response(Duration::from_millis(500))?; + Ok((established, server)) +} + #[test] fn validated_peer_close_frame_yields_nonforgeable_transport_observation() -> Result<(), Box> { @@ -88,6 +114,34 @@ fn validated_peer_close_frame_yields_nonforgeable_transport_observation() Ok(()) } +#[test] +fn peer_close_without_tcp_eof_is_not_transport_closure_evidence() -> Result<(), Box> { + let (established, server) = established_with_held_open_peer_close()?; + + let result = WebDriverBiDiWebSocketTransportClosureObservation::observe( + established, + Duration::from_millis(50), + ); + + server + .join() + .map_err(|_| io::Error::other("held-open close test server panicked"))??; + let error = result + .err() + .ok_or_else(|| io::Error::other("peer Close alone became final transport-closure evidence"))?; + assert!(matches!( + &error, + WebDriverBiDiWebSocketTransportClosureError::Frame { + source: WebDriverBiDiWebSocketFrameError::FrameReadTimedOut { bytes_read: 0, .. } + } + )); + assert_eq!( + error.to_string(), + "WebDriver BiDi transport closure could not be observed safely" + ); + Ok(()) +} + #[test] fn one_unsolicited_pong_before_close_does_not_block_closure_observation() -> Result<(), Box> { From 8d63ec8942d556bb9029fa4f9eb033d246b1aa60 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 8 Sep 2026 03:24:25 +0900 Subject: [PATCH 074/105] test(network): require Pong before bounded close completion --- ...webdriver_bidi_transport_close_evidence.rs | 93 ++++++++++++++++++- 1 file changed, 90 insertions(+), 3 deletions(-) diff --git a/crates/originweave-network/tests/webdriver_bidi_transport_close_evidence.rs b/crates/originweave-network/tests/webdriver_bidi_transport_close_evidence.rs index b6327a22e..e1a090fa5 100644 --- a/crates/originweave-network/tests/webdriver_bidi_transport_close_evidence.rs +++ b/crates/originweave-network/tests/webdriver_bidi_transport_close_evidence.rs @@ -40,6 +40,35 @@ fn read_opening_request(stream: &mut TcpStream) -> io::Result<()> { Ok(()) } +fn read_masked_control_frame( + stream: &mut TcpStream, + expected_opcode: u8, +) -> io::Result> { + let mut header = [0_u8; 2]; + stream.read_exact(&mut header)?; + if header[0] != (0x80 | expected_opcode) || header[1] & 0x80 == 0 { + return Err(io::Error::new( + io::ErrorKind::InvalidData, + "expected one final masked client control frame", + )); + } + let payload_len = usize::from(header[1] & 0x7f); + if payload_len > 125 { + return Err(io::Error::new( + io::ErrorKind::InvalidData, + "client control-frame payload exceeded RFC 6455 bound", + )); + } + let mut mask = [0_u8; 4]; + stream.read_exact(&mut mask)?; + let mut payload = vec![0_u8; payload_len]; + stream.read_exact(&mut payload)?; + for (index, byte) in payload.iter_mut().enumerate() { + *byte ^= mask[index % mask.len()]; + } + Ok(payload) +} + fn established_with_server_frame( frame: Option<&'static [u8]>, ) -> Result> { @@ -93,6 +122,64 @@ fn established_with_held_open_peer_close() -> Result Result> { + let listener = TcpListener::bind(("127.0.0.1", 0))?; + let local_addr = listener.local_addr()?; + let server = thread::spawn(move || -> io::Result<()> { + let (mut stream, _) = listener.accept()?; + read_opening_request(&mut stream)?; + stream.write_all(OPENING_RESPONSE)?; + stream.write_all(&[0x89, 0x05, b'p', b'r', b'o', b'b', b'e'])?; + let pong_payload = read_masked_control_frame(&mut stream, 0x0a)?; + if pong_payload != b"probe" { + return Err(io::Error::new( + io::ErrorKind::InvalidData, + "Pong did not preserve the Ping application data", + )); + } + stream.write_all(&[0x88, 0x02, 0x03, 0xe8])?; + thread::sleep(Duration::from_millis(250)); + Ok(()) + }); + + let endpoint = format!("ws://{local_addr}/session/{SESSION_ID}"); + let target = WebDriverBiDiWebSocketEndpoint::new(&endpoint)? + .correlate_session_id(SESSION_ID)? + .into_explicit_connect_target()?; + let connection = + WebDriverBiDiTcpConnectionPlan::new(target, Duration::from_secs(1), 1)?.connect()?; + let key = WebDriverBiDiWebSocketClientKey::new(RFC6455_SAMPLE_KEY)?; + let established = WebDriverBiDiWebSocketHandshakePlan::new(connection, key)? + .write_opening_request(Duration::from_millis(500))? + .read_opening_response(Duration::from_millis(500))?; + Ok((established, server)) +} + +#[test] +fn ping_before_close_requires_masked_pong_and_still_waits_for_tcp_closure() +-> Result<(), Box> { + let (established, server) = established_with_ping_then_held_open_peer_close()?; + + let result = WebDriverBiDiWebSocketTransportClosureObservation::observe( + established, + Duration::from_millis(50), + ); + let server_result = server + .join() + .map_err(|_| io::Error::other("ping-before-close test server panicked"))?; + let error = result.err().ok_or_else(|| { + io::Error::other("Ping/Close traffic became final transport-closure evidence") + })?; + assert!(matches!( + &error, + WebDriverBiDiWebSocketTransportClosureError::Frame { + source: WebDriverBiDiWebSocketFrameError::FrameReadTimedOut { bytes_read: 0, .. } + } + )); + server_result?; + Ok(()) +} + #[test] fn validated_peer_close_frame_yields_nonforgeable_transport_observation() -> Result<(), Box> { @@ -126,9 +213,9 @@ fn peer_close_without_tcp_eof_is_not_transport_closure_evidence() -> Result<(), server .join() .map_err(|_| io::Error::other("held-open close test server panicked"))??; - let error = result - .err() - .ok_or_else(|| io::Error::other("peer Close alone became final transport-closure evidence"))?; + let error = result.err().ok_or_else(|| { + io::Error::other("peer Close alone became final transport-closure evidence") + })?; assert!(matches!( &error, WebDriverBiDiWebSocketTransportClosureError::Frame { From 1cb76a600854cc519bd13051d33970b5d600a50a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 8 Sep 2026 03:46:30 +0900 Subject: [PATCH 075/105] fix(network): complete bounded WebSocket closing handshake --- .../src/webdriver_bidi_websocket_frame.rs | 21 ++ ...driver_bidi_websocket_transport_closure.rs | 133 +++++++--- ...driver_bidi_session_teardown_assessment.rs | 83 ++++--- ...webdriver_bidi_transport_close_evidence.rs | 228 ++++++++++++++---- 4 files changed, 343 insertions(+), 122 deletions(-) diff --git a/crates/originweave-network/src/webdriver_bidi_websocket_frame.rs b/crates/originweave-network/src/webdriver_bidi_websocket_frame.rs index 4a680daed..cd0fc8574 100644 --- a/crates/originweave-network/src/webdriver_bidi_websocket_frame.rs +++ b/crates/originweave-network/src/webdriver_bidi_websocket_frame.rs @@ -284,6 +284,27 @@ impl WebDriverBiDiWebSocketEstablished { write_frame_with_clock(&mut self.raw.stream, &frame, frame_timeout, &mut now).map(|_| self) } + /// Write one final masked RFC 6455 Close response on this verified stream. + /// + /// This crate-private operation is used only after the frame reader has validated a peer Close. + /// It echoes only the validated status code when one was present and deliberately does not replay + /// arbitrary peer reason text. The caller supplies fresh masking entropy; the same adjacent-key + /// guard used by all client frame writers remains in force. + pub(crate) fn write_close_frame( + mut self, + peer_close_status_code: Option, + masking_key: WebDriverBiDiWebSocketMaskKey, + frame_timeout: Duration, + ) -> Result { + validate_frame_timeout(frame_timeout)?; + self.client_mask_keys.reserve(masking_key)?; + let status_bytes = peer_close_status_code.map(u16::to_be_bytes); + let payload = status_bytes.as_ref().map_or(&[][..], |bytes| bytes.as_slice()); + let frame = serialize_client_frame(0x8, payload, masking_key); + let mut now = Instant::now; + write_frame_with_clock(&mut self.raw.stream, &frame, frame_timeout, &mut now).map(|_| self) + } + /// Read one bounded RFC 6455 frame from this verified stream. /// /// Server frames must be unmasked. Reserved bits/opcodes, non-minimal lengths, oversized diff --git a/crates/originweave-network/src/webdriver_bidi_websocket_transport_closure.rs b/crates/originweave-network/src/webdriver_bidi_websocket_transport_closure.rs index e3298c304..7e971abf0 100644 --- a/crates/originweave-network/src/webdriver_bidi_websocket_transport_closure.rs +++ b/crates/originweave-network/src/webdriver_bidi_websocket_transport_closure.rs @@ -2,31 +2,33 @@ use std::{error::Error, fmt, time::Duration}; use crate::{ WebDriverBiDiWebSocketEstablished, WebDriverBiDiWebSocketFrameError, + WebDriverBiDiWebSocketMaskKey, webdriver_bidi_connection::WebDriverBiDiConnectionGeneration, }; /// Bounded transport-closure condition observed on one consumed WebDriver BiDi WebSocket. /// -/// This classification proves only what the already session-correlated transport itself exposed. -/// A peer Close frame does not prove browser-process exit or profile cleanup, while peer EOF does -/// not imply that the RFC 6455 closing handshake completed. +/// Every successful variant proves clean TCP EOF on the exact established connection. A preceding +/// RFC 6455 Close exchange is retained separately from EOF-only cessation so a consumer cannot +/// confuse entering CLOSING with the transport actually becoming closed. #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub enum WebDriverBiDiWebSocketTransportClosureKind { - /// The peer sent one RFC 6455 Close frame that passed the existing strict frame validator. - PeerCloseFrame, /// The peer ended the TCP byte stream cleanly before any new WebSocket frame byte was read. PeerEof, + /// A validated peer Close was answered with a masked client Close before clean TCP EOF. + PeerCloseThenEof, } -/// Credential-free observation that one established WebDriver BiDi transport ceased carrying data. +/// Credential-free observation that one established WebDriver BiDi transport actually closed. /// /// Construction consumes the established WebSocket, so this value cannot be used to regain the /// underlying connection. It retains the private process-local generation of that exact connection /// so a later teardown consumer can reject closure observed on another socket even when session and -/// command identifiers are reused. It records only a validated peer Close status when one was -/// actually present, or clean TCP EOF before a new frame began. It grants no browser, process, -/// profile, policy, secret, retry, reconnect, or Agent authority and does not perform a reciprocal -/// Close handshake. +/// command identifiers are reused. Peer Close enters CLOSING only: OriginWeave answers it with a +/// caller-keyed masked Close and emits this final observation only after subsequent clean TCP EOF. +/// Pre-Close Ping is answered with an equally caller-keyed masked Pong carrying identical payload. +/// No masking entropy, browser authority, process state, profile cleanup, policy, secret, reconnect, +/// retry, or Agent authority is invented by this transport adapter. #[derive(Debug, Eq, PartialEq)] pub struct WebDriverBiDiWebSocketTransportClosureObservation { kind: WebDriverBiDiWebSocketTransportClosureKind, @@ -35,49 +37,85 @@ pub struct WebDriverBiDiWebSocketTransportClosureObservation { } impl WebDriverBiDiWebSocketTransportClosureObservation { - /// Consume one established connection and observe one bounded transport-closure condition. + /// Consume one established connection and prove bounded transport closure. /// - /// A validated peer Close frame and zero-byte clean EOF are the only success cases. One - /// unsolicited Pong may be ignored before that closure signal because Pong requires no client - /// response; a second Pong, Ping, data frame, partial-frame EOF, timeout, malformed Close, I/O - /// failure, or integrity failure remains a typed error. This fixed two-read envelope prevents - /// peer control traffic from extending teardown observation indefinitely and does not invent - /// masking entropy or outbound authority to answer Ping frames. + /// The caller supplies independent fresh masking keys for the only pre-Close Ping response and + /// the required Close response. At most one pre-Close Ping or unsolicited Pong is admitted; + /// additional control traffic, application data, timeout, partial EOF, malformed frames, write + /// failures, or any post-Close frame fail closed. Peer Close alone is never success: after the + /// masked response this boundary requires zero-byte TCP EOF within the same bounded frame + /// deadline before producing closure evidence. pub fn observe( established: WebDriverBiDiWebSocketEstablished, + pong_masking_key: WebDriverBiDiWebSocketMaskKey, + close_masking_key: WebDriverBiDiWebSocketMaskKey, frame_timeout: Duration, ) -> Result { let connection_generation = established.transport_evidence().connection_generation(); - Self::observe_frame(established, frame_timeout, true, connection_generation) + Self::observe_pre_close( + established, + pong_masking_key, + close_masking_key, + frame_timeout, + true, + connection_generation, + ) } - fn observe_frame( + fn observe_pre_close( established: WebDriverBiDiWebSocketEstablished, + pong_masking_key: WebDriverBiDiWebSocketMaskKey, + close_masking_key: WebDriverBiDiWebSocketMaskKey, frame_timeout: Duration, - allow_pong: bool, + allow_pre_close_control: bool, connection_generation: WebDriverBiDiConnectionGeneration, ) -> Result { match established.read_frame(frame_timeout) { - Ok((established, frame)) if frame.opcode() == 0xa && allow_pong => { - Self::observe_frame(established, frame_timeout, false, connection_generation) + Ok((established, frame)) if frame.opcode() == 0xa && allow_pre_close_control => { + Self::observe_pre_close( + established, + pong_masking_key, + close_masking_key, + frame_timeout, + false, + connection_generation, + ) + } + Ok((established, frame)) if frame.opcode() == 0x9 && allow_pre_close_control => { + let established = established + .write_pong_frame(frame.payload(), pong_masking_key, frame_timeout) + .map_err(|source| WebDriverBiDiWebSocketTransportClosureError::Frame { + source, + })?; + Self::observe_pre_close( + established, + pong_masking_key, + close_masking_key, + frame_timeout, + false, + connection_generation, + ) } - Ok((established, frame)) => { - if frame.opcode() != 0x8 { - return Err( - WebDriverBiDiWebSocketTransportClosureError::UnexpectedFrame { - opcode: frame.opcode(), - }, - ); - } + Ok((established, frame)) if frame.opcode() == 0x8 => { let peer_close_status_code = frame .payload() .get(..2) .map(|bytes| u16::from_be_bytes([bytes[0], bytes[1]])); - drop(established); - Ok(Self { - kind: WebDriverBiDiWebSocketTransportClosureKind::PeerCloseFrame, + let established = established + .write_close_frame(peer_close_status_code, close_masking_key, frame_timeout) + .map_err(|source| WebDriverBiDiWebSocketTransportClosureError::Frame { + source, + })?; + Self::observe_eof_after_close( + established, + frame_timeout, peer_close_status_code, connection_generation, + ) + } + Ok((_established, frame)) => { + Err(WebDriverBiDiWebSocketTransportClosureError::UnexpectedFrame { + opcode: frame.opcode(), }) } Err(WebDriverBiDiWebSocketFrameError::FrameEnded { bytes_read: 0 }) => Ok(Self { @@ -89,13 +127,34 @@ impl WebDriverBiDiWebSocketTransportClosureObservation { } } + fn observe_eof_after_close( + established: WebDriverBiDiWebSocketEstablished, + frame_timeout: Duration, + peer_close_status_code: Option, + connection_generation: WebDriverBiDiConnectionGeneration, + ) -> Result { + match established.read_frame(frame_timeout) { + Err(WebDriverBiDiWebSocketFrameError::FrameEnded { bytes_read: 0 }) => Ok(Self { + kind: WebDriverBiDiWebSocketTransportClosureKind::PeerCloseThenEof, + peer_close_status_code, + connection_generation, + }), + Ok((_established, frame)) => { + Err(WebDriverBiDiWebSocketTransportClosureError::UnexpectedFrame { + opcode: frame.opcode(), + }) + } + Err(source) => Err(WebDriverBiDiWebSocketTransportClosureError::Frame { source }), + } + } + /// Return the exact transport-closure condition that produced this observation. #[must_use] pub const fn kind(&self) -> WebDriverBiDiWebSocketTransportClosureKind { self.kind } - /// Return the validated peer Close status when a Close frame actually carried one. + /// Return the validated peer Close status when the completed closing exchange carried one. #[must_use] pub const fn peer_close_status_code(&self) -> Option { self.peer_close_status_code @@ -109,12 +168,12 @@ impl WebDriverBiDiWebSocketTransportClosureObservation { /// Fail-closed errors while converting one established BiDi transport into closure evidence. #[derive(Debug)] pub enum WebDriverBiDiWebSocketTransportClosureError { - /// The peer sent a valid WebSocket frame that was not an admissible closure signal. + /// The peer sent a valid WebSocket frame outside the bounded closing state machine. UnexpectedFrame { - /// Exact validated RFC 6455 opcode observed instead of an admissible closure signal. + /// Exact validated RFC 6455 opcode observed instead of admissible bounded closing traffic. opcode: u8, }, - /// The existing bounded WebSocket frame reader failed before closure was proven. + /// The existing bounded WebSocket frame reader or writer failed before closure was proven. Frame { /// Original typed frame failure retained as the causal source. source: WebDriverBiDiWebSocketFrameError, diff --git a/crates/originweave-network/tests/webdriver_bidi_session_teardown_assessment.rs b/crates/originweave-network/tests/webdriver_bidi_session_teardown_assessment.rs index 90c17b6c0..ddcf481e5 100644 --- a/crates/originweave-network/tests/webdriver_bidi_session_teardown_assessment.rs +++ b/crates/originweave-network/tests/webdriver_bidi_session_teardown_assessment.rs @@ -24,6 +24,14 @@ const RFC6455_SAMPLE_KEY: &str = "dGhlIHNhbXBsZSBub25jZQ=="; const OPENING_RESPONSE: &[u8] = b"HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=\r\n\r\n"; const END_SUCCESS_RESPONSE: &[u8] = br#"{"type":"success","id":7,"result":{}}"#; const NORMAL_CLOSE_FRAME: &[u8] = &[0x88, 0x02, 0x03, 0xe8]; +const PONG_MASK_KEY: [u8; 4] = [5, 6, 7, 8]; +const CLOSE_MASK_KEY: [u8; 4] = [9, 10, 11, 12]; + +type SessionEndFixture = ( + WebDriverBiDiSessionEndResult, + WebDriverBiDiWebSocketEstablished, + thread::JoinHandle>, +); fn read_opening_request(stream: &mut TcpStream) -> io::Result<()> { stream.set_read_timeout(Some(Duration::from_secs(2)))?; @@ -71,13 +79,7 @@ fn read_masked_text_frame(stream: &mut TcpStream) -> io::Result> { fn correlated_session_end_ack_and_transport_on( listener: &TcpListener, session_id: &str, -) -> Result< - ( - WebDriverBiDiSessionEndResult, - WebDriverBiDiWebSocketEstablished, - ), - Box, -> { +) -> Result> { let local_addr = listener.local_addr()?; let server_listener = listener.try_clone()?; let server = thread::spawn(move || -> io::Result<()> { @@ -93,7 +95,9 @@ fn correlated_session_end_ack_and_transport_on( } stream.write_all(&[0x81, END_SUCCESS_RESPONSE.len() as u8])?; stream.write_all(END_SUCCESS_RESPONSE)?; - stream.write_all(NORMAL_CLOSE_FRAME) + stream.write_all(NORMAL_CLOSE_FRAME)?; + thread::sleep(Duration::from_millis(50)); + Ok(()) }); let endpoint = format!("ws://{local_addr}/session/{session_id}"); @@ -131,45 +135,41 @@ fn correlated_session_end_ack_and_transport_on( } }; let acknowledged = WebDriverBiDiSessionEndResult::parse_and_correlate(&text, &mut correlation)?; - server - .join() - .map_err(|_| io::Error::other("session.end teardown test server panicked"))??; - Ok((acknowledged, established)) + Ok((acknowledged, established, server)) } fn correlated_session_end_ack_and_transport_for( session_id: &str, -) -> Result< - ( - WebDriverBiDiSessionEndResult, - WebDriverBiDiWebSocketEstablished, - ), - Box, -> { +) -> Result> { let listener = TcpListener::bind(("127.0.0.1", 0))?; correlated_session_end_ack_and_transport_on(&listener, session_id) } -fn correlated_session_end_ack_and_transport() -> Result< - ( - WebDriverBiDiSessionEndResult, - WebDriverBiDiWebSocketEstablished, - ), - Box, -> { +fn correlated_session_end_ack_and_transport() -> Result> { correlated_session_end_ack_and_transport_for(SESSION_ID) } +fn join_server(server: thread::JoinHandle>) -> Result<(), Box> { + server + .join() + .map_err(|_| io::Error::other("session.end teardown test server panicked"))??; + Ok(()) +} + fn observed_transport_closure( established: WebDriverBiDiWebSocketEstablished, + server: thread::JoinHandle>, ) -> Result> { let observation = WebDriverBiDiWebSocketTransportClosureObservation::observe( established, + WebDriverBiDiWebSocketMaskKey::new(PONG_MASK_KEY), + WebDriverBiDiWebSocketMaskKey::new(CLOSE_MASK_KEY), Duration::from_millis(500), )?; + join_server(server)?; assert_eq!( observation.kind(), - WebDriverBiDiWebSocketTransportClosureKind::PeerCloseFrame + WebDriverBiDiWebSocketTransportClosureKind::PeerCloseThenEof ); assert_eq!(observation.peer_close_status_code(), Some(1000)); Ok(observation) @@ -178,11 +178,12 @@ fn observed_transport_closure( #[test] fn missing_typed_transport_observation_keeps_teardown_pending() -> Result<(), Box> { for transport_closed in [false, true] { - let (acknowledged, established) = correlated_session_end_ack_and_transport()?; + let (acknowledged, established, server) = correlated_session_end_ack_and_transport()?; let transport_closure = if transport_closed { - Some(observed_transport_closure(established)?) + Some(observed_transport_closure(established, server)?) } else { drop(established); + join_server(server)?; None }; let assessment = WebDriverBiDiSessionTeardownAssessment::from_protocol_ack( @@ -200,7 +201,7 @@ fn missing_typed_transport_observation_keeps_teardown_pending() -> Result<(), Bo .observations() .transport_closure_observation() .map(WebDriverBiDiWebSocketTransportClosureObservation::kind), - transport_closed.then_some(WebDriverBiDiWebSocketTransportClosureKind::PeerCloseFrame) + transport_closed.then_some(WebDriverBiDiWebSocketTransportClosureKind::PeerCloseThenEof) ); assert!(!assessment.is_operationally_complete()); assert_eq!( @@ -213,8 +214,8 @@ fn missing_typed_transport_observation_keeps_teardown_pending() -> Result<(), Bo #[test] fn typed_closure_still_requires_process_and_profile_evidence() -> Result<(), Box> { - let (acknowledged, established) = correlated_session_end_ack_and_transport()?; - let transport_closure = observed_transport_closure(established)?; + let (acknowledged, established, server) = correlated_session_end_ack_and_transport()?; + let transport_closure = observed_transport_closure(established, server)?; let assessment = WebDriverBiDiSessionTeardownAssessment::from_protocol_ack( acknowledged, WebDriverBiDiSessionTeardownObservations::new(Some(transport_closure)), @@ -239,8 +240,9 @@ fn typed_closure_still_requires_process_and_profile_evidence() -> Result<(), Box fn assert_cross_connection_closure_rejected( acknowledged: WebDriverBiDiSessionEndResult, foreign_established: WebDriverBiDiWebSocketEstablished, + foreign_server: thread::JoinHandle>, ) -> Result<(), Box> { - let foreign_closure = observed_transport_closure(foreign_established)?; + let foreign_closure = observed_transport_closure(foreign_established, foreign_server)?; let result = WebDriverBiDiSessionTeardownAssessment::from_protocol_ack( acknowledged, WebDriverBiDiSessionTeardownObservations::new(Some(foreign_closure)), @@ -264,19 +266,22 @@ fn assert_cross_connection_closure_rejected( #[test] fn same_session_reconnect_cannot_supply_prior_closure() -> Result<(), Box> { let listener = TcpListener::bind(("127.0.0.1", 0))?; - let (acknowledged_a, established_a) = + let (acknowledged_a, established_a, server_a) = correlated_session_end_ack_and_transport_on(&listener, SESSION_ID)?; drop(established_a); - let (_acknowledged_b, established_b) = + join_server(server_a)?; + let (_acknowledged_b, established_b, server_b) = correlated_session_end_ack_and_transport_on(&listener, SESSION_ID)?; - assert_cross_connection_closure_rejected(acknowledged_a, established_b) + assert_cross_connection_closure_rejected(acknowledged_a, established_b, server_b) } #[test] fn another_session_cannot_supply_acknowledged_transport_closure() -> Result<(), Box> { - let (acknowledged_a, established_a) = correlated_session_end_ack_and_transport_for(SESSION_ID)?; + let (acknowledged_a, established_a, server_a) = + correlated_session_end_ack_and_transport_for(SESSION_ID)?; drop(established_a); - let (_acknowledged_b, established_b) = + join_server(server_a)?; + let (_acknowledged_b, established_b, server_b) = correlated_session_end_ack_and_transport_for(SECOND_SESSION_ID)?; - assert_cross_connection_closure_rejected(acknowledged_a, established_b) + assert_cross_connection_closure_rejected(acknowledged_a, established_b, server_b) } diff --git a/crates/originweave-network/tests/webdriver_bidi_transport_close_evidence.rs b/crates/originweave-network/tests/webdriver_bidi_transport_close_evidence.rs index e1a090fa5..71469eca3 100644 --- a/crates/originweave-network/tests/webdriver_bidi_transport_close_evidence.rs +++ b/crates/originweave-network/tests/webdriver_bidi_transport_close_evidence.rs @@ -10,13 +10,15 @@ use originweave_core::WebDriverBiDiWebSocketEndpoint; use originweave_network::{ WebDriverBiDiTcpConnectionPlan, WebDriverBiDiWebSocketClientKey, WebDriverBiDiWebSocketFrameError, WebDriverBiDiWebSocketHandshakePlan, - WebDriverBiDiWebSocketTransportClosureError, WebDriverBiDiWebSocketTransportClosureKind, - WebDriverBiDiWebSocketTransportClosureObservation, + WebDriverBiDiWebSocketMaskKey, WebDriverBiDiWebSocketTransportClosureError, + WebDriverBiDiWebSocketTransportClosureKind, WebDriverBiDiWebSocketTransportClosureObservation, }; const SESSION_ID: &str = "01234567-89ab-cdef-0123-456789abcdef"; const RFC6455_SAMPLE_KEY: &str = "dGhlIHNhbXBsZSBub25jZQ=="; const OPENING_RESPONSE: &[u8] = b"HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=\r\n\r\n"; +const PONG_MASK_KEY: [u8; 4] = [5, 6, 7, 8]; +const CLOSE_MASK_KEY: [u8; 4] = [9, 10, 11, 12]; type EstablishedWithServer = ( originweave_network::WebDriverBiDiWebSocketEstablished, @@ -40,10 +42,7 @@ fn read_opening_request(stream: &mut TcpStream) -> io::Result<()> { Ok(()) } -fn read_masked_control_frame( - stream: &mut TcpStream, - expected_opcode: u8, -) -> io::Result> { +fn read_masked_control_frame(stream: &mut TcpStream, expected_opcode: u8) -> io::Result> { let mut header = [0_u8; 2]; stream.read_exact(&mut header)?; if header[0] != (0x80 | expected_opcode) || header[1] & 0x80 == 0 { @@ -97,6 +96,40 @@ fn established_with_server_frame( Ok((established, server)) } +fn established_with_peer_close_then_eof( + close_frame: &'static [u8], + expected_reply_payload: &'static [u8], +) -> Result> { + let listener = TcpListener::bind(("127.0.0.1", 0))?; + let local_addr = listener.local_addr()?; + let server = thread::spawn(move || -> io::Result<()> { + let (mut stream, _) = listener.accept()?; + read_opening_request(&mut stream)?; + stream.write_all(OPENING_RESPONSE)?; + stream.write_all(close_frame)?; + let close_reply = read_masked_control_frame(&mut stream, 0x08)?; + if close_reply != expected_reply_payload { + return Err(io::Error::new( + io::ErrorKind::InvalidData, + "Close response did not preserve the validated peer status contract", + )); + } + Ok(()) + }); + + let endpoint = format!("ws://{local_addr}/session/{SESSION_ID}"); + let target = WebDriverBiDiWebSocketEndpoint::new(&endpoint)? + .correlate_session_id(SESSION_ID)? + .into_explicit_connect_target()?; + let connection = + WebDriverBiDiTcpConnectionPlan::new(target, Duration::from_secs(1), 1)?.connect()?; + let key = WebDriverBiDiWebSocketClientKey::new(RFC6455_SAMPLE_KEY)?; + let established = WebDriverBiDiWebSocketHandshakePlan::new(connection, key)? + .write_opening_request(Duration::from_millis(500))? + .read_opening_response(Duration::from_millis(500))?; + Ok((established, server)) +} + fn established_with_held_open_peer_close() -> Result> { let listener = TcpListener::bind(("127.0.0.1", 0))?; let local_addr = listener.local_addr()?; @@ -105,6 +138,13 @@ fn established_with_held_open_peer_close() -> Result Result Result> { +fn established_with_ping_then_held_open_peer_close() -> Result> +{ let listener = TcpListener::bind(("127.0.0.1", 0))?; let local_addr = listener.local_addr()?; let server = thread::spawn(move || -> io::Result<()> { @@ -138,6 +179,13 @@ fn established_with_ping_then_held_open_peer_close() -> Result Result Result> { + let listener = TcpListener::bind(("127.0.0.1", 0))?; + let local_addr = listener.local_addr()?; + let server = thread::spawn(move || -> io::Result<()> { + let (mut stream, _) = listener.accept()?; + read_opening_request(&mut stream)?; + stream.write_all(OPENING_RESPONSE)?; + stream.write_all(&[0x8a, 0x00, 0x88, 0x02, 0x03, 0xe8])?; + let close_reply = read_masked_control_frame(&mut stream, 0x08)?; + if close_reply != [0x03, 0xe8] { + return Err(io::Error::new( + io::ErrorKind::InvalidData, + "Pong/Close peer did not receive the required Close response", + )); + } + Ok(()) + }); + + let endpoint = format!("ws://{local_addr}/session/{SESSION_ID}"); + let target = WebDriverBiDiWebSocketEndpoint::new(&endpoint)? + .correlate_session_id(SESSION_ID)? + .into_explicit_connect_target()?; + let connection = + WebDriverBiDiTcpConnectionPlan::new(target, Duration::from_secs(1), 1)?.connect()?; + let key = WebDriverBiDiWebSocketClientKey::new(RFC6455_SAMPLE_KEY)?; + let established = WebDriverBiDiWebSocketHandshakePlan::new(connection, key)? + .write_opening_request(Duration::from_millis(500))? + .read_opening_response(Duration::from_millis(500))?; + Ok((established, server)) +} + +fn established_with_post_close_frame() -> Result> { + let listener = TcpListener::bind(("127.0.0.1", 0))?; + let local_addr = listener.local_addr()?; + let server = thread::spawn(move || -> io::Result<()> { + let (mut stream, _) = listener.accept()?; + read_opening_request(&mut stream)?; + stream.write_all(OPENING_RESPONSE)?; + stream.write_all(&[0x88, 0x02, 0x03, 0xe8])?; + let close_reply = read_masked_control_frame(&mut stream, 0x08)?; + if close_reply != [0x03, 0xe8] { + return Err(io::Error::new( + io::ErrorKind::InvalidData, + "post-Close fixture did not receive the required Close response", + )); + } + stream.write_all(&[0x8a, 0x00])?; + Ok(()) + }); + + let endpoint = format!("ws://{local_addr}/session/{SESSION_ID}"); + let target = WebDriverBiDiWebSocketEndpoint::new(&endpoint)? + .correlate_session_id(SESSION_ID)? + .into_explicit_connect_target()?; + let connection = + WebDriverBiDiTcpConnectionPlan::new(target, Duration::from_secs(1), 1)?.connect()?; + let key = WebDriverBiDiWebSocketClientKey::new(RFC6455_SAMPLE_KEY)?; + let established = WebDriverBiDiWebSocketHandshakePlan::new(connection, key)? + .write_opening_request(Duration::from_millis(500))? + .read_opening_response(Duration::from_millis(500))?; + Ok((established, server)) +} + +fn observe( + established: originweave_network::WebDriverBiDiWebSocketEstablished, + frame_timeout: Duration, +) -> Result< + WebDriverBiDiWebSocketTransportClosureObservation, + WebDriverBiDiWebSocketTransportClosureError, +> { + WebDriverBiDiWebSocketTransportClosureObservation::observe( + established, + WebDriverBiDiWebSocketMaskKey::new(PONG_MASK_KEY), + WebDriverBiDiWebSocketMaskKey::new(CLOSE_MASK_KEY), + frame_timeout, + ) +} + #[test] fn ping_before_close_requires_masked_pong_and_still_waits_for_tcp_closure() -> Result<(), Box> { let (established, server) = established_with_ping_then_held_open_peer_close()?; - let result = WebDriverBiDiWebSocketTransportClosureObservation::observe( - established, - Duration::from_millis(50), - ); + let result = observe(established, Duration::from_millis(50)); let server_result = server .join() .map_err(|_| io::Error::other("ping-before-close test server panicked"))?; @@ -181,34 +304,45 @@ fn ping_before_close_requires_masked_pong_and_still_waits_for_tcp_closure() } #[test] -fn validated_peer_close_frame_yields_nonforgeable_transport_observation() --> Result<(), Box> { - let (established, server) = established_with_server_frame(Some(&[0x88, 0x02, 0x03, 0xe8]))?; +fn validated_peer_close_requires_masked_reply_and_tcp_eof() -> Result<(), Box> { + let (established, server) = + established_with_peer_close_then_eof(&[0x88, 0x04, 0x03, 0xe8, b'o', b'k'], &[0x03, 0xe8])?; - let observation = WebDriverBiDiWebSocketTransportClosureObservation::observe( - established, - Duration::from_millis(500), - )?; + let observation = observe(established, Duration::from_millis(500))?; server .join() .map_err(|_| io::Error::other("transport-close test server panicked"))??; assert_eq!( observation.kind(), - WebDriverBiDiWebSocketTransportClosureKind::PeerCloseFrame + WebDriverBiDiWebSocketTransportClosureKind::PeerCloseThenEof ); assert_eq!(observation.peer_close_status_code(), Some(1000)); Ok(()) } +#[test] +fn empty_peer_close_uses_empty_masked_reply_before_tcp_eof() -> Result<(), Box> { + let (established, server) = established_with_peer_close_then_eof(&[0x88, 0x00], &[])?; + + let observation = observe(established, Duration::from_millis(500))?; + + server + .join() + .map_err(|_| io::Error::other("empty-close test server panicked"))??; + assert_eq!( + observation.kind(), + WebDriverBiDiWebSocketTransportClosureKind::PeerCloseThenEof + ); + assert_eq!(observation.peer_close_status_code(), None); + Ok(()) +} + #[test] fn peer_close_without_tcp_eof_is_not_transport_closure_evidence() -> Result<(), Box> { let (established, server) = established_with_held_open_peer_close()?; - let result = WebDriverBiDiWebSocketTransportClosureObservation::observe( - established, - Duration::from_millis(50), - ); + let result = observe(established, Duration::from_millis(50)); server .join() @@ -232,20 +366,16 @@ fn peer_close_without_tcp_eof_is_not_transport_closure_evidence() -> Result<(), #[test] fn one_unsolicited_pong_before_close_does_not_block_closure_observation() -> Result<(), Box> { - let (established, server) = - established_with_server_frame(Some(&[0x8a, 0x00, 0x88, 0x02, 0x03, 0xe8]))?; + let (established, server) = established_with_pong_then_peer_close()?; - let observation = WebDriverBiDiWebSocketTransportClosureObservation::observe( - established, - Duration::from_millis(500), - )?; + let observation = observe(established, Duration::from_millis(500))?; server .join() .map_err(|_| io::Error::other("pong-before-close test server panicked"))??; assert_eq!( observation.kind(), - WebDriverBiDiWebSocketTransportClosureKind::PeerCloseFrame + WebDriverBiDiWebSocketTransportClosureKind::PeerCloseThenEof ); assert_eq!(observation.peer_close_status_code(), Some(1000)); Ok(()) @@ -255,10 +385,7 @@ fn one_unsolicited_pong_before_close_does_not_block_closure_observation() fn repeated_pong_frames_remain_fail_closed_under_fixed_read_budget() -> Result<(), Box> { let (established, server) = established_with_server_frame(Some(&[0x8a, 0x00, 0x8a, 0x00]))?; - let Err(error) = WebDriverBiDiWebSocketTransportClosureObservation::observe( - established, - Duration::from_millis(500), - ) else { + let Err(error) = observe(established, Duration::from_millis(500)) else { return Err(io::Error::other( "repeated Pong frames unexpectedly became transport-closure evidence", ) @@ -285,10 +412,7 @@ fn clean_peer_eof_yields_transport_observation_without_inventing_close_status() -> Result<(), Box> { let (established, server) = established_with_server_frame(None)?; - let observation = WebDriverBiDiWebSocketTransportClosureObservation::observe( - established, - Duration::from_millis(500), - )?; + let observation = observe(established, Duration::from_millis(500))?; server .join() @@ -301,15 +425,30 @@ fn clean_peer_eof_yields_transport_observation_without_inventing_close_status() Ok(()) } +#[test] +fn post_close_frame_is_not_transport_closure_evidence() -> Result<(), Box> { + let (established, server) = established_with_post_close_frame()?; + + let Err(error) = observe(established, Duration::from_millis(500)) else { + return Err(io::Error::other("post-Close frame unexpectedly became closure evidence").into()); + }; + + server + .join() + .map_err(|_| io::Error::other("post-Close frame test server panicked"))??; + assert!(matches!( + error, + WebDriverBiDiWebSocketTransportClosureError::UnexpectedFrame { opcode: 0xa } + )); + Ok(()) +} + #[test] fn application_frame_after_teardown_does_not_become_closure_evidence() -> Result<(), Box> { let (established, server) = established_with_server_frame(Some(&[0x81, 0x02, b'o', b'k']))?; - let Err(error) = WebDriverBiDiWebSocketTransportClosureObservation::observe( - established, - Duration::from_millis(500), - ) else { + let Err(error) = observe(established, Duration::from_millis(500)) else { return Err(io::Error::other( "application frame unexpectedly became transport-closure evidence", ) @@ -335,10 +474,7 @@ fn application_frame_after_teardown_does_not_become_closure_evidence() -> Result fn malformed_close_frame_remains_a_typed_frame_failure() -> Result<(), Box> { let (established, server) = established_with_server_frame(Some(&[0x88, 0x01, 0x00]))?; - let Err(error) = WebDriverBiDiWebSocketTransportClosureObservation::observe( - established, - Duration::from_millis(500), - ) else { + let Err(error) = observe(established, Duration::from_millis(500)) else { return Err(io::Error::other( "malformed close frame unexpectedly became transport-closure evidence", ) From 1376f4c223382007e438f58b397d90483c0c5bdd Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 8 Sep 2026 07:09:42 +0900 Subject: [PATCH 076/105] test(network): complete teardown close fixture --- ...driver_bidi_websocket_transport_closure.rs | 31 +++++----- ...driver_bidi_session_teardown_assessment.rs | 62 +++++++++++++++---- ...webdriver_bidi_transport_close_evidence.rs | 4 +- 3 files changed, 69 insertions(+), 28 deletions(-) diff --git a/crates/originweave-network/src/webdriver_bidi_websocket_transport_closure.rs b/crates/originweave-network/src/webdriver_bidi_websocket_transport_closure.rs index 7e971abf0..bb5a1f0fa 100644 --- a/crates/originweave-network/src/webdriver_bidi_websocket_transport_closure.rs +++ b/crates/originweave-network/src/webdriver_bidi_websocket_transport_closure.rs @@ -2,8 +2,7 @@ use std::{error::Error, fmt, time::Duration}; use crate::{ WebDriverBiDiWebSocketEstablished, WebDriverBiDiWebSocketFrameError, - WebDriverBiDiWebSocketMaskKey, - webdriver_bidi_connection::WebDriverBiDiConnectionGeneration, + WebDriverBiDiWebSocketMaskKey, webdriver_bidi_connection::WebDriverBiDiConnectionGeneration, }; /// Bounded transport-closure condition observed on one consumed WebDriver BiDi WebSocket. @@ -84,9 +83,9 @@ impl WebDriverBiDiWebSocketTransportClosureObservation { Ok((established, frame)) if frame.opcode() == 0x9 && allow_pre_close_control => { let established = established .write_pong_frame(frame.payload(), pong_masking_key, frame_timeout) - .map_err(|source| WebDriverBiDiWebSocketTransportClosureError::Frame { - source, - })?; + .map_err( + |source| WebDriverBiDiWebSocketTransportClosureError::Frame { source }, + )?; Self::observe_pre_close( established, pong_masking_key, @@ -103,9 +102,9 @@ impl WebDriverBiDiWebSocketTransportClosureObservation { .map(|bytes| u16::from_be_bytes([bytes[0], bytes[1]])); let established = established .write_close_frame(peer_close_status_code, close_masking_key, frame_timeout) - .map_err(|source| WebDriverBiDiWebSocketTransportClosureError::Frame { - source, - })?; + .map_err( + |source| WebDriverBiDiWebSocketTransportClosureError::Frame { source }, + )?; Self::observe_eof_after_close( established, frame_timeout, @@ -113,11 +112,11 @@ impl WebDriverBiDiWebSocketTransportClosureObservation { connection_generation, ) } - Ok((_established, frame)) => { - Err(WebDriverBiDiWebSocketTransportClosureError::UnexpectedFrame { + Ok((_established, frame)) => Err( + WebDriverBiDiWebSocketTransportClosureError::UnexpectedFrame { opcode: frame.opcode(), - }) - } + }, + ), Err(WebDriverBiDiWebSocketFrameError::FrameEnded { bytes_read: 0 }) => Ok(Self { kind: WebDriverBiDiWebSocketTransportClosureKind::PeerEof, peer_close_status_code: None, @@ -139,11 +138,11 @@ impl WebDriverBiDiWebSocketTransportClosureObservation { peer_close_status_code, connection_generation, }), - Ok((_established, frame)) => { - Err(WebDriverBiDiWebSocketTransportClosureError::UnexpectedFrame { + Ok((_established, frame)) => Err( + WebDriverBiDiWebSocketTransportClosureError::UnexpectedFrame { opcode: frame.opcode(), - }) - } + }, + ), Err(source) => Err(WebDriverBiDiWebSocketTransportClosureError::Frame { source }), } } diff --git a/crates/originweave-network/tests/webdriver_bidi_session_teardown_assessment.rs b/crates/originweave-network/tests/webdriver_bidi_session_teardown_assessment.rs index ddcf481e5..9c48f8818 100644 --- a/crates/originweave-network/tests/webdriver_bidi_session_teardown_assessment.rs +++ b/crates/originweave-network/tests/webdriver_bidi_session_teardown_assessment.rs @@ -76,9 +76,36 @@ fn read_masked_text_frame(stream: &mut TcpStream) -> io::Result> { Ok(payload) } +fn read_masked_close_frame(stream: &mut TcpStream) -> io::Result> { + let mut header = [0_u8; 2]; + stream.read_exact(&mut header)?; + if header[0] != 0x88 || header[1] & 0x80 == 0 { + return Err(io::Error::new( + io::ErrorKind::InvalidData, + "expected one final masked client Close frame", + )); + } + let length = usize::from(header[1] & 0x7f); + if length > 125 { + return Err(io::Error::new( + io::ErrorKind::InvalidData, + "client Close response exceeded the RFC 6455 control-frame bound", + )); + } + let mut mask = [0_u8; 4]; + stream.read_exact(&mut mask)?; + let mut payload = vec![0_u8; length]; + stream.read_exact(&mut payload)?; + for (index, byte) in payload.iter_mut().enumerate() { + *byte ^= mask[index % mask.len()]; + } + Ok(payload) +} + fn correlated_session_end_ack_and_transport_on( listener: &TcpListener, session_id: &str, + expect_close_response: bool, ) -> Result> { let local_addr = listener.local_addr()?; let server_listener = listener.try_clone()?; @@ -96,7 +123,15 @@ fn correlated_session_end_ack_and_transport_on( stream.write_all(&[0x81, END_SUCCESS_RESPONSE.len() as u8])?; stream.write_all(END_SUCCESS_RESPONSE)?; stream.write_all(NORMAL_CLOSE_FRAME)?; - thread::sleep(Duration::from_millis(50)); + if expect_close_response { + let close_reply = read_masked_close_frame(&mut stream)?; + if close_reply != [0x03, 0xe8] { + return Err(io::Error::new( + io::ErrorKind::InvalidData, + "session teardown did not return Close(1000)", + )); + } + } Ok(()) }); @@ -140,13 +175,16 @@ fn correlated_session_end_ack_and_transport_on( fn correlated_session_end_ack_and_transport_for( session_id: &str, + expect_close_response: bool, ) -> Result> { let listener = TcpListener::bind(("127.0.0.1", 0))?; - correlated_session_end_ack_and_transport_on(&listener, session_id) + correlated_session_end_ack_and_transport_on(&listener, session_id, expect_close_response) } -fn correlated_session_end_ack_and_transport() -> Result> { - correlated_session_end_ack_and_transport_for(SESSION_ID) +fn correlated_session_end_ack_and_transport( + expect_close_response: bool, +) -> Result> { + correlated_session_end_ack_and_transport_for(SESSION_ID, expect_close_response) } fn join_server(server: thread::JoinHandle>) -> Result<(), Box> { @@ -178,7 +216,8 @@ fn observed_transport_closure( #[test] fn missing_typed_transport_observation_keeps_teardown_pending() -> Result<(), Box> { for transport_closed in [false, true] { - let (acknowledged, established, server) = correlated_session_end_ack_and_transport()?; + let (acknowledged, established, server) = + correlated_session_end_ack_and_transport(transport_closed)?; let transport_closure = if transport_closed { Some(observed_transport_closure(established, server)?) } else { @@ -201,7 +240,8 @@ fn missing_typed_transport_observation_keeps_teardown_pending() -> Result<(), Bo .observations() .transport_closure_observation() .map(WebDriverBiDiWebSocketTransportClosureObservation::kind), - transport_closed.then_some(WebDriverBiDiWebSocketTransportClosureKind::PeerCloseThenEof) + transport_closed + .then_some(WebDriverBiDiWebSocketTransportClosureKind::PeerCloseThenEof) ); assert!(!assessment.is_operationally_complete()); assert_eq!( @@ -214,7 +254,7 @@ fn missing_typed_transport_observation_keeps_teardown_pending() -> Result<(), Bo #[test] fn typed_closure_still_requires_process_and_profile_evidence() -> Result<(), Box> { - let (acknowledged, established, server) = correlated_session_end_ack_and_transport()?; + let (acknowledged, established, server) = correlated_session_end_ack_and_transport(true)?; let transport_closure = observed_transport_closure(established, server)?; let assessment = WebDriverBiDiSessionTeardownAssessment::from_protocol_ack( acknowledged, @@ -267,21 +307,21 @@ fn assert_cross_connection_closure_rejected( fn same_session_reconnect_cannot_supply_prior_closure() -> Result<(), Box> { let listener = TcpListener::bind(("127.0.0.1", 0))?; let (acknowledged_a, established_a, server_a) = - correlated_session_end_ack_and_transport_on(&listener, SESSION_ID)?; + correlated_session_end_ack_and_transport_on(&listener, SESSION_ID, false)?; drop(established_a); join_server(server_a)?; let (_acknowledged_b, established_b, server_b) = - correlated_session_end_ack_and_transport_on(&listener, SESSION_ID)?; + correlated_session_end_ack_and_transport_on(&listener, SESSION_ID, true)?; assert_cross_connection_closure_rejected(acknowledged_a, established_b, server_b) } #[test] fn another_session_cannot_supply_acknowledged_transport_closure() -> Result<(), Box> { let (acknowledged_a, established_a, server_a) = - correlated_session_end_ack_and_transport_for(SESSION_ID)?; + correlated_session_end_ack_and_transport_for(SESSION_ID, false)?; drop(established_a); join_server(server_a)?; let (_acknowledged_b, established_b, server_b) = - correlated_session_end_ack_and_transport_for(SECOND_SESSION_ID)?; + correlated_session_end_ack_and_transport_for(SECOND_SESSION_ID, true)?; assert_cross_connection_closure_rejected(acknowledged_a, established_b, server_b) } diff --git a/crates/originweave-network/tests/webdriver_bidi_transport_close_evidence.rs b/crates/originweave-network/tests/webdriver_bidi_transport_close_evidence.rs index 71469eca3..677115404 100644 --- a/crates/originweave-network/tests/webdriver_bidi_transport_close_evidence.rs +++ b/crates/originweave-network/tests/webdriver_bidi_transport_close_evidence.rs @@ -430,7 +430,9 @@ fn post_close_frame_is_not_transport_closure_evidence() -> Result<(), Box Date: Tue, 8 Sep 2026 09:19:25 +0900 Subject: [PATCH 077/105] style(network): apply rustfmt to close frame Signed-off-by: Seongho Bae --- .../originweave-network/src/webdriver_bidi_websocket_frame.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/crates/originweave-network/src/webdriver_bidi_websocket_frame.rs b/crates/originweave-network/src/webdriver_bidi_websocket_frame.rs index cd0fc8574..ee5d67194 100644 --- a/crates/originweave-network/src/webdriver_bidi_websocket_frame.rs +++ b/crates/originweave-network/src/webdriver_bidi_websocket_frame.rs @@ -299,7 +299,9 @@ impl WebDriverBiDiWebSocketEstablished { validate_frame_timeout(frame_timeout)?; self.client_mask_keys.reserve(masking_key)?; let status_bytes = peer_close_status_code.map(u16::to_be_bytes); - let payload = status_bytes.as_ref().map_or(&[][..], |bytes| bytes.as_slice()); + let payload = status_bytes + .as_ref() + .map_or(&[][..], |bytes| bytes.as_slice()); let frame = serialize_client_frame(0x8, payload, masking_key); let mut now = Instant::now; write_frame_with_clock(&mut self.raw.stream, &frame, frame_timeout, &mut now).map(|_| self) From 1dcaaf826bb13b27e443401c0e208ec4528bd497 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 8 Sep 2026 09:20:19 +0900 Subject: [PATCH 078/105] fix(network): retain connection-bound response correlation Signed-off-by: Seongho Bae --- .../src/webdriver_bidi_command_correlation.rs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/crates/originweave-network/src/webdriver_bidi_command_correlation.rs b/crates/originweave-network/src/webdriver_bidi_command_correlation.rs index fe1e13492..e08c17534 100644 --- a/crates/originweave-network/src/webdriver_bidi_command_correlation.rs +++ b/crates/originweave-network/src/webdriver_bidi_command_correlation.rs @@ -261,6 +261,21 @@ impl WebDriverBiDiCommandCorrelation { } } + pub(crate) fn correlate_response_for_connection( + &mut self, + envelope: &WebDriverBiDiJsonEnvelope, + expected_kind: WebDriverBiDiCommandKind, + received_connection_generation: WebDriverBiDiConnectionGeneration, + ) -> Result { + let (command_id, outcome) = response_route(envelope)?; + self.complete_on_connection( + command_id, + expected_kind, + outcome, + received_connection_generation, + ) + } + fn require_command_kind( &self, command_id: u64, From ec16b3392a1e9589a04d8b84ad7dd5b575e047f7 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 8 Sep 2026 09:21:08 +0900 Subject: [PATCH 079/105] fix(network): restore shared response routing Signed-off-by: Seongho Bae --- .../src/webdriver_bidi_command_correlation.rs | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/crates/originweave-network/src/webdriver_bidi_command_correlation.rs b/crates/originweave-network/src/webdriver_bidi_command_correlation.rs index e08c17534..9cd3ebc16 100644 --- a/crates/originweave-network/src/webdriver_bidi_command_correlation.rs +++ b/crates/originweave-network/src/webdriver_bidi_command_correlation.rs @@ -32,6 +32,25 @@ struct OutstandingCommand { connection_generation: Option, } +fn response_route( + envelope: &WebDriverBiDiJsonEnvelope, +) -> Result<(u64, WebDriverBiDiCorrelatedResponseOutcome), WebDriverBiDiCommandCorrelationError> { + match envelope.routing() { + WebDriverBiDiJsonEnvelopeRouting::Event => { + Err(WebDriverBiDiCommandCorrelationError::EventIsNotResponse) + } + WebDriverBiDiJsonEnvelopeRouting::CommandError { command_id: None } => { + Err(WebDriverBiDiCommandCorrelationError::UncorrelatableErrorResponse) + } + WebDriverBiDiJsonEnvelopeRouting::CommandError { + command_id: Some(command_id), + } => Ok((command_id, WebDriverBiDiCorrelatedResponseOutcome::Error)), + WebDriverBiDiJsonEnvelopeRouting::CommandSuccess { command_id } => { + Ok((command_id, WebDriverBiDiCorrelatedResponseOutcome::Success)) + } + } +} + /// Outcome of a response after it has consumed the matching outstanding command identifier. #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub enum WebDriverBiDiCorrelatedResponseOutcome { From bae183eb8e208f9a81dd43389ef60a61fcf4a2db Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 8 Sep 2026 09:34:19 +0900 Subject: [PATCH 080/105] test(network): cover teardown mask-key failures Signed-off-by: Seongho Bae --- ...webdriver_bidi_transport_close_evidence.rs | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/crates/originweave-network/tests/webdriver_bidi_transport_close_evidence.rs b/crates/originweave-network/tests/webdriver_bidi_transport_close_evidence.rs index 677115404..c40dca726 100644 --- a/crates/originweave-network/tests/webdriver_bidi_transport_close_evidence.rs +++ b/crates/originweave-network/tests/webdriver_bidi_transport_close_evidence.rs @@ -497,3 +497,43 @@ fn malformed_close_frame_remains_a_typed_frame_failure() -> Result<(), Box Result<(), Box> { + let (established, server) = established_with_ping_then_held_open_peer_close()?; + let reused = WebDriverBiDiWebSocketMaskKey::new(PONG_MASK_KEY); + let established = established.write_text_frame("{}", reused, Duration::from_millis(500))?; + + let result = WebDriverBiDiWebSocketTransportClosureObservation::observe( + established, + reused, + WebDriverBiDiWebSocketMaskKey::new(CLOSE_MASK_KEY), + Duration::from_millis(500), + ); + assert!(matches!( + result, + Err(WebDriverBiDiWebSocketTransportClosureError::Frame { .. }) + )); + let _ = server.join(); + Ok(()) +} + +#[test] +fn reused_close_mask_key_preserves_typed_frame_failure() -> Result<(), Box> { + let (established, server) = established_with_ping_then_held_open_peer_close()?; + let reused = WebDriverBiDiWebSocketMaskKey::new(CLOSE_MASK_KEY); + let established = established.write_text_frame("{}", reused, Duration::from_millis(500))?; + + let result = WebDriverBiDiWebSocketTransportClosureObservation::observe( + established, + WebDriverBiDiWebSocketMaskKey::new(PONG_MASK_KEY), + reused, + Duration::from_millis(500), + ); + assert!(matches!( + result, + Err(WebDriverBiDiWebSocketTransportClosureError::Frame { .. }) + )); + let _ = server.join(); + Ok(()) +} From f060bf1c992357e2c05ee2946e1daef91e39bea3 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 8 Sep 2026 09:40:25 +0900 Subject: [PATCH 081/105] test(network): cover peer-close mask-key failure Signed-off-by: Seongho Bae --- ...webdriver_bidi_transport_close_evidence.rs | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/crates/originweave-network/tests/webdriver_bidi_transport_close_evidence.rs b/crates/originweave-network/tests/webdriver_bidi_transport_close_evidence.rs index c40dca726..159c3ec0c 100644 --- a/crates/originweave-network/tests/webdriver_bidi_transport_close_evidence.rs +++ b/crates/originweave-network/tests/webdriver_bidi_transport_close_evidence.rs @@ -537,3 +537,24 @@ fn reused_close_mask_key_preserves_typed_frame_failure() -> Result<(), Box Result<(), Box> +{ + let (established, server) = established_with_server_frame(Some(&[0x88, 0x02, 0x03, 0xe8]))?; + let reused = WebDriverBiDiWebSocketMaskKey::new(CLOSE_MASK_KEY); + let established = established.write_text_frame("{}", reused, Duration::from_millis(500))?; + + let result = WebDriverBiDiWebSocketTransportClosureObservation::observe( + established, + WebDriverBiDiWebSocketMaskKey::new(PONG_MASK_KEY), + reused, + Duration::from_millis(500), + ); + assert!(matches!( + result, + Err(WebDriverBiDiWebSocketTransportClosureError::Frame { .. }) + )); + let _ = server.join(); + Ok(()) +} From 4a7cc03d7f2969101c0da20085b72a257c2b73ee Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 8 Sep 2026 09:49:19 +0900 Subject: [PATCH 082/105] test(network): cover bounded pre-close control branch Signed-off-by: Seongho Bae --- .../tests/webdriver_bidi_transport_close_evidence.rs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/crates/originweave-network/tests/webdriver_bidi_transport_close_evidence.rs b/crates/originweave-network/tests/webdriver_bidi_transport_close_evidence.rs index 159c3ec0c..39d304a6a 100644 --- a/crates/originweave-network/tests/webdriver_bidi_transport_close_evidence.rs +++ b/crates/originweave-network/tests/webdriver_bidi_transport_close_evidence.rs @@ -407,6 +407,18 @@ fn repeated_pong_frames_remain_fail_closed_under_fixed_read_budget() -> Result<( Ok(()) } +#[test] +fn ping_after_pre_close_pong_remains_fail_closed() -> Result<(), Box> { + let (established, server) = established_with_server_frame(Some(&[0x8a, 0x00, 0x89, 0x00]))?; + let result = observe(established, Duration::from_millis(500)); + let _ = server.join(); + assert!(matches!( + result, + Err(WebDriverBiDiWebSocketTransportClosureError::UnexpectedFrame { opcode: 0x9 }) + )); + Ok(()) +} + #[test] fn clean_peer_eof_yields_transport_observation_without_inventing_close_status() -> Result<(), Box> { From ac5cb34e43dd23f1b3ab28843d03eb99e92e1a0c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 8 Sep 2026 09:55:29 +0900 Subject: [PATCH 083/105] test(network): cover close timeout validation Signed-off-by: Seongho Bae --- .../tests/webdriver_bidi_transport_close_evidence.rs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/crates/originweave-network/tests/webdriver_bidi_transport_close_evidence.rs b/crates/originweave-network/tests/webdriver_bidi_transport_close_evidence.rs index 39d304a6a..a34c41df7 100644 --- a/crates/originweave-network/tests/webdriver_bidi_transport_close_evidence.rs +++ b/crates/originweave-network/tests/webdriver_bidi_transport_close_evidence.rs @@ -419,6 +419,18 @@ fn ping_after_pre_close_pong_remains_fail_closed() -> Result<(), Box> Ok(()) } +#[test] +fn peer_close_rejects_zero_frame_timeout_before_writing() -> Result<(), Box> { + let (established, server) = established_with_server_frame(Some(&[0x88, 0x02, 0x03, 0xe8]))?; + let result = observe(established, Duration::ZERO); + let _ = server.join(); + assert!(matches!( + result, + Err(WebDriverBiDiWebSocketTransportClosureError::Frame { .. }) + )); + Ok(()) +} + #[test] fn clean_peer_eof_yields_transport_observation_without_inventing_close_status() -> Result<(), Box> { From 2fb1b593717f6b14711d4f92b0ae63ca42640be6 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 8 Sep 2026 10:00:13 +0900 Subject: [PATCH 084/105] test(network): cover excessive close timeout Signed-off-by: Seongho Bae --- .../tests/webdriver_bidi_transport_close_evidence.rs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/crates/originweave-network/tests/webdriver_bidi_transport_close_evidence.rs b/crates/originweave-network/tests/webdriver_bidi_transport_close_evidence.rs index a34c41df7..2a93af8fa 100644 --- a/crates/originweave-network/tests/webdriver_bidi_transport_close_evidence.rs +++ b/crates/originweave-network/tests/webdriver_bidi_transport_close_evidence.rs @@ -431,6 +431,18 @@ fn peer_close_rejects_zero_frame_timeout_before_writing() -> Result<(), Box Result<(), Box> { + let (established, server) = established_with_server_frame(Some(&[0x88, 0x02, 0x03, 0xe8]))?; + let result = observe(established, Duration::from_secs(6)); + let _ = server.join(); + assert!(matches!( + result, + Err(WebDriverBiDiWebSocketTransportClosureError::Frame { .. }) + )); + Ok(()) +} + #[test] fn clean_peer_eof_yields_transport_observation_without_inventing_close_status() -> Result<(), Box> { From a17914adec018d72509398312e659ec9dc9476a4 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 8 Sep 2026 10:04:45 +0900 Subject: [PATCH 085/105] fix(network): separate frame timeout guards Signed-off-by: Seongho Bae --- .../src/webdriver_bidi_websocket_frame.rs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/crates/originweave-network/src/webdriver_bidi_websocket_frame.rs b/crates/originweave-network/src/webdriver_bidi_websocket_frame.rs index ee5d67194..fd04d23f4 100644 --- a/crates/originweave-network/src/webdriver_bidi_websocket_frame.rs +++ b/crates/originweave-network/src/webdriver_bidi_websocket_frame.rs @@ -493,7 +493,13 @@ impl Error for WebDriverBiDiWebSocketFrameError { } fn validate_frame_timeout(frame_timeout: Duration) -> Result<(), WebDriverBiDiWebSocketFrameError> { - if frame_timeout.is_zero() || frame_timeout > MAX_WEBSOCKET_FRAME_TIMEOUT { + if frame_timeout.is_zero() { + return Err(WebDriverBiDiWebSocketFrameError::InvalidFrameTimeout { + frame_timeout, + maximum_timeout: MAX_WEBSOCKET_FRAME_TIMEOUT, + }); + } + if frame_timeout > MAX_WEBSOCKET_FRAME_TIMEOUT { return Err(WebDriverBiDiWebSocketFrameError::InvalidFrameTimeout { frame_timeout, maximum_timeout: MAX_WEBSOCKET_FRAME_TIMEOUT, From 9033fbc7a8158a40293eedf279773ab43515a106 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 8 Sep 2026 10:10:48 +0900 Subject: [PATCH 086/105] fix(network): make timeout validation exhaustive Signed-off-by: Seongho Bae --- .../src/webdriver_bidi_websocket_frame.rs | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/crates/originweave-network/src/webdriver_bidi_websocket_frame.rs b/crates/originweave-network/src/webdriver_bidi_websocket_frame.rs index fd04d23f4..b24355232 100644 --- a/crates/originweave-network/src/webdriver_bidi_websocket_frame.rs +++ b/crates/originweave-network/src/webdriver_bidi_websocket_frame.rs @@ -493,13 +493,14 @@ impl Error for WebDriverBiDiWebSocketFrameError { } fn validate_frame_timeout(frame_timeout: Duration) -> Result<(), WebDriverBiDiWebSocketFrameError> { - if frame_timeout.is_zero() { - return Err(WebDriverBiDiWebSocketFrameError::InvalidFrameTimeout { - frame_timeout, - maximum_timeout: MAX_WEBSOCKET_FRAME_TIMEOUT, - }); - } - if frame_timeout > MAX_WEBSOCKET_FRAME_TIMEOUT { + let invalid = match ( + frame_timeout.is_zero(), + frame_timeout > MAX_WEBSOCKET_FRAME_TIMEOUT, + ) { + (true, _) | (false, true) => true, + (false, false) => false, + }; + if invalid { return Err(WebDriverBiDiWebSocketFrameError::InvalidFrameTimeout { frame_timeout, maximum_timeout: MAX_WEBSOCKET_FRAME_TIMEOUT, From 2bbdc026128de445a380e6f12626d64c03bcbc62 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 8 Sep 2026 10:26:44 +0900 Subject: [PATCH 087/105] fix(network): avoid timeout guard short circuit Signed-off-by: Seongho Bae --- .../src/webdriver_bidi_websocket_frame.rs | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/crates/originweave-network/src/webdriver_bidi_websocket_frame.rs b/crates/originweave-network/src/webdriver_bidi_websocket_frame.rs index b24355232..7787f349b 100644 --- a/crates/originweave-network/src/webdriver_bidi_websocket_frame.rs +++ b/crates/originweave-network/src/webdriver_bidi_websocket_frame.rs @@ -493,14 +493,8 @@ impl Error for WebDriverBiDiWebSocketFrameError { } fn validate_frame_timeout(frame_timeout: Duration) -> Result<(), WebDriverBiDiWebSocketFrameError> { - let invalid = match ( - frame_timeout.is_zero(), - frame_timeout > MAX_WEBSOCKET_FRAME_TIMEOUT, - ) { - (true, _) | (false, true) => true, - (false, false) => false, - }; - if invalid { + let valid = !frame_timeout.is_zero() & (frame_timeout <= MAX_WEBSOCKET_FRAME_TIMEOUT); + if !valid { return Err(WebDriverBiDiWebSocketFrameError::InvalidFrameTimeout { frame_timeout, maximum_timeout: MAX_WEBSOCKET_FRAME_TIMEOUT, From 6cdf6743a9113044e0940275c686bfc332792fef Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 8 Sep 2026 10:34:25 +0900 Subject: [PATCH 088/105] test(network): verify close writer deadline rejection directly Co-Authored-By: OpenAI Codex Signed-off-by: Seongho Bae --- AGENTS.md | 4 ++ CHANGELOG.md | 2 + .../src/webdriver_bidi_websocket_frame.rs | 69 ++++++++++++++++++- 3 files changed, 73 insertions(+), 2 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 6f747c38e..67b72b84e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -115,4 +115,8 @@ A skipped security, GPU, browser, TLS, or statistical test is not passing eviden ## Release contract +### Coverage diagnosis lesson + +Aggregate LLVM code regions by source coordinates across function instantiations before identifying a missing path. An invalid-input test at a public entry point may stop at an earlier guard; it does not prove a later private writer's error return executed. Exercise that writer directly, assert the exact error, and verify the peer received no bytes. Do not rewrite production predicates based only on a file-level coverage deficit. + A release requires all current-head checks, complete coverage and docs, updated `CHANGELOG.md`, SBOM and provenance, reproducible artifacts, compatibility evidence, security review, and an explicit version decision. Pre-alpha commits are not releases. diff --git a/CHANGELOG.md b/CHANGELOG.md index 354d1f1f0..d8f17e9ff 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,8 @@ All notable changes to OriginWeave are documented in this file. The format follo ### Changed +- Restored the simple frame-timeout validation after correcting coverage diagnosis; direct Close-writer tests verify invalid deadlines send no bytes and return the expected error. + - Removed an unused private correlated-response accessor while retaining connection-generation validation at the receiving-message boundary, and corrected the Rust `AtomicU64` standard-library reference to its canonical type-alias page. - Integrated the current teardown prerequisites into transport-closure observation, including the previously uncollected release-record check, while retaining the unresolved connection-provenance finding and its downstream repair ownership. - Integrated the verified opening-exchange and closure prerequisites into the connection-bound response repair, preserving its sender, receiver and closure provenance checks while restoring the inherited executable release contract; process and profile cleanup remain unproven. diff --git a/crates/originweave-network/src/webdriver_bidi_websocket_frame.rs b/crates/originweave-network/src/webdriver_bidi_websocket_frame.rs index 7787f349b..989f28833 100644 --- a/crates/originweave-network/src/webdriver_bidi_websocket_frame.rs +++ b/crates/originweave-network/src/webdriver_bidi_websocket_frame.rs @@ -493,8 +493,7 @@ impl Error for WebDriverBiDiWebSocketFrameError { } fn validate_frame_timeout(frame_timeout: Duration) -> Result<(), WebDriverBiDiWebSocketFrameError> { - let valid = !frame_timeout.is_zero() & (frame_timeout <= MAX_WEBSOCKET_FRAME_TIMEOUT); - if !valid { + if frame_timeout.is_zero() || frame_timeout > MAX_WEBSOCKET_FRAME_TIMEOUT { return Err(WebDriverBiDiWebSocketFrameError::InvalidFrameTimeout { frame_timeout, maximum_timeout: MAX_WEBSOCKET_FRAME_TIMEOUT, @@ -827,6 +826,72 @@ mod tests { use super::*; + #[test] + fn close_writer_rejects_invalid_deadline_without_emitting_bytes() { + use crate::WebDriverBiDiTcpConnectionPlan; + use originweave_core::WebDriverBiDiWebSocketEndpoint; + use std::net::{TcpListener, TcpStream}; + use std::thread; + + for timeout in [ + Duration::ZERO, + MAX_WEBSOCKET_FRAME_TIMEOUT + Duration::from_nanos(1), + ] { + let listener = TcpListener::bind("127.0.0.1:0").expect("bind peer"); + let address = listener.local_addr().expect("peer address"); + let peer = thread::spawn(move || { + let (mut stream, _) = listener.accept().expect("accept client"); + stream + .set_read_timeout(Some(Duration::from_secs(2))) + .expect("bound peer read"); + let mut request = Vec::new(); + while !request.ends_with(b"\r\n\r\n") { + let mut byte = [0]; + stream.read_exact(&mut byte).expect("opening request"); + request.push(byte[0]); + } + stream.write_all(b"HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=\r\n\r\n").expect("opening response"); + let mut byte = [0]; + assert_eq!( + TcpStream::read(&mut stream, &mut byte).expect("client EOF"), + 0 + ); + }); + let session = "01234567-89ab-cdef-0123-456789abcdef"; + let endpoint = + WebDriverBiDiWebSocketEndpoint::new(&format!("ws://{address}/session/{session}")) + .expect("endpoint"); + let target = endpoint + .correlate_session_id(session) + .expect("session") + .into_explicit_connect_target() + .expect("target"); + let connection = WebDriverBiDiTcpConnectionPlan::new(target, Duration::from_secs(1), 1) + .expect("plan") + .connect() + .expect("connect"); + let key = crate::WebDriverBiDiWebSocketClientKey::new("dGhlIHNhbXBsZSBub25jZQ==") + .expect("key"); + let established = WebDriverBiDiWebSocketHandshakePlan::new(connection, key) + .expect("handshake") + .write_opening_request(Duration::from_secs(1)) + .expect("write opening") + .read_opening_response(Duration::from_secs(1)) + .expect("read opening"); + let error = established + .write_close_frame( + Some(1000), + WebDriverBiDiWebSocketMaskKey::new([1, 2, 3, 4]), + timeout, + ) + .expect_err("reject deadline"); + assert!( + matches!(error, WebDriverBiDiWebSocketFrameError::InvalidFrameTimeout { frame_timeout, maximum_timeout } if frame_timeout == timeout && maximum_timeout == MAX_WEBSOCKET_FRAME_TIMEOUT) + ); + peer.join().expect("peer completed"); + } + } + #[derive(Clone, Debug)] enum ReadAction { Bytes(Vec), From 7a88551505d541f7a9a83d79d35b701fd03b59bd Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 8 Sep 2026 10:54:27 +0900 Subject: [PATCH 089/105] test(network): preserve exact close deadline assertions Replace compound test guards with full typed error debug equality after exact-head coverage identified unexecuted assertion branches. Production validation and peer EOF checks are unchanged. Co-Authored-By: OpenAI Codex --- AGENTS.md | 6 ++++-- CHANGELOG.md | 1 + .../src/webdriver_bidi_websocket_frame.rs | 11 +++++++++-- 3 files changed, 14 insertions(+), 4 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 67b72b84e..c2a1af957 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -113,10 +113,12 @@ A skipped security, GPU, browser, TLS, or statistical test is not passing eviden - Run reasoning-effort and orchestration-depth ablations before claiming an LLM path is superior. - Scheduled agents may create bounded reviewed PRs but may not merge, tag, publish, alter workflows, add secrets, or weaken checks. -## Release contract - ### Coverage diagnosis lesson Aggregate LLVM code regions by source coordinates across function instantiations before identifying a missing path. An invalid-input test at a public entry point may stop at an earlier guard; it does not prove a later private writer's error return executed. Exercise that writer directly, assert the exact error, and verify the peer received no bytes. Do not rewrite production predicates based only on a file-level coverage deficit. +Inspect uncovered coordinates inside test assertions too: guarded `matches!` expressions can contribute never-taken failure branches to the file summary. Preserve exact variant and field checks rather than broadening the accepted error to make coverage pass. + +## Release contract + A release requires all current-head checks, complete coverage and docs, updated `CHANGELOG.md`, SBOM and provenance, reproducible artifacts, compatibility evidence, security review, and an explicit version decision. Pre-alpha commits are not releases. diff --git a/CHANGELOG.md b/CHANGELOG.md index d8f17e9ff..74573b243 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ All notable changes to OriginWeave are documented in this file. The format follo ### Changed - Restored the simple frame-timeout validation after correcting coverage diagnosis; direct Close-writer tests verify invalid deadlines send no bytes and return the expected error. +- Kept exact invalid-deadline error checks without compound test-only guards in the coverage measurement. - Removed an unused private correlated-response accessor while retaining connection-generation validation at the receiving-message boundary, and corrected the Rust `AtomicU64` standard-library reference to its canonical type-alias page. - Integrated the current teardown prerequisites into transport-closure observation, including the previously uncollected release-record check, while retaining the unresolved connection-provenance finding and its downstream repair ownership. diff --git a/crates/originweave-network/src/webdriver_bidi_websocket_frame.rs b/crates/originweave-network/src/webdriver_bidi_websocket_frame.rs index 989f28833..b70ead2ac 100644 --- a/crates/originweave-network/src/webdriver_bidi_websocket_frame.rs +++ b/crates/originweave-network/src/webdriver_bidi_websocket_frame.rs @@ -885,8 +885,15 @@ mod tests { timeout, ) .expect_err("reject deadline"); - assert!( - matches!(error, WebDriverBiDiWebSocketFrameError::InvalidFrameTimeout { frame_timeout, maximum_timeout } if frame_timeout == timeout && maximum_timeout == MAX_WEBSOCKET_FRAME_TIMEOUT) + assert_eq!( + format!("{error:?}"), + format!( + "{:?}", + WebDriverBiDiWebSocketFrameError::InvalidFrameTimeout { + frame_timeout: timeout, + maximum_timeout: MAX_WEBSOCKET_FRAME_TIMEOUT, + } + ) ); peer.join().expect("peer completed"); } From d126242c7198c447d0fab7983d529441340fd1c9 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 8 Sep 2026 11:14:52 +0900 Subject: [PATCH 090/105] test(network): exercise complete close writer contract Cover successful Close frames with and without status, invalid deadlines, and adjacent mask reuse in the unit binary. Assert literal peer bytes and EOF so complementary instantiations are not mistaken for complete summary coverage. Co-Authored-By: OpenAI Codex --- AGENTS.md | 2 + CHANGELOG.md | 1 + .../src/webdriver_bidi_websocket_frame.rs | 83 +++++++++++++++---- 3 files changed, 69 insertions(+), 17 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index c2a1af957..a421fe3f6 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -117,6 +117,8 @@ A skipped security, GPU, browser, TLS, or statistical test is not passing eviden Aggregate LLVM code regions by source coordinates across function instantiations before identifying a missing path. An invalid-input test at a public entry point may stop at an earlier guard; it does not prove a later private writer's error return executed. Exercise that writer directly, assert the exact error, and verify the peer received no bytes. Do not rewrite production predicates based only on a file-level coverage deficit. +Coordinate union alone does not reproduce LLVM's region summary: `RegionCoverageInfo::merge` takes maximum covered/total counts across instantiations. Complementary unit-test and integration-test executions can therefore leave a deficit. Exercise successful status-bearing and empty Close writes, invalid deadlines, and adjacent masking-key rejection in the same unit-test binary; compare literal wire bytes and join the peer. Reference: LLVM Project. (n.d.). *CoverageSummaryInfo.h* [Source code]. https://github.com/llvm/llvm-project/blob/main/llvm/tools/llvm-cov/CoverageSummaryInfo.h + Inspect uncovered coordinates inside test assertions too: guarded `matches!` expressions can contribute never-taken failure branches to the file summary. Preserve exact variant and field checks rather than broadening the accepted error to make coverage pass. ## Release contract diff --git a/CHANGELOG.md b/CHANGELOG.md index 74573b243..341404065 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ All notable changes to OriginWeave are documented in this file. The format follo - Restored the simple frame-timeout validation after correcting coverage diagnosis; direct Close-writer tests verify invalid deadlines send no bytes and return the expected error. - Kept exact invalid-deadline error checks without compound test-only guards in the coverage measurement. +- Verified literal masked Close bytes with and without a status code, and that a reused masking key emits no Close bytes after the preceding text frame. - Removed an unused private correlated-response accessor while retaining connection-generation validation at the receiving-message boundary, and corrected the Rust `AtomicU64` standard-library reference to its canonical type-alias page. - Integrated the current teardown prerequisites into transport-closure observation, including the previously uncollected release-record check, while retaining the unresolved connection-provenance finding and its downstream repair ownership. diff --git a/crates/originweave-network/src/webdriver_bidi_websocket_frame.rs b/crates/originweave-network/src/webdriver_bidi_websocket_frame.rs index b70ead2ac..3f5c6f717 100644 --- a/crates/originweave-network/src/webdriver_bidi_websocket_frame.rs +++ b/crates/originweave-network/src/webdriver_bidi_websocket_frame.rs @@ -827,15 +827,57 @@ mod tests { use super::*; #[test] - fn close_writer_rejects_invalid_deadline_without_emitting_bytes() { + fn close_writer_checks_deadlines_masks_and_exact_wire_bytes() { use crate::WebDriverBiDiTcpConnectionPlan; use originweave_core::WebDriverBiDiWebSocketEndpoint; use std::net::{TcpListener, TcpStream}; use std::thread; - for timeout in [ - Duration::ZERO, - MAX_WEBSOCKET_FRAME_TIMEOUT + Duration::from_nanos(1), + let excessive = MAX_WEBSOCKET_FRAME_TIMEOUT + Duration::from_nanos(1); + for (timeout, status, seed_text, expected_bytes, expected_result) in [ + ( + Duration::ZERO, + Some(1000), + false, + vec![], + Err(WebDriverBiDiWebSocketFrameError::InvalidFrameTimeout { + frame_timeout: Duration::ZERO, + maximum_timeout: MAX_WEBSOCKET_FRAME_TIMEOUT, + }), + ), + ( + excessive, + Some(1000), + false, + vec![], + Err(WebDriverBiDiWebSocketFrameError::InvalidFrameTimeout { + frame_timeout: excessive, + maximum_timeout: MAX_WEBSOCKET_FRAME_TIMEOUT, + }), + ), + ( + Duration::from_secs(1), + Some(1000), + false, + vec![0x88, 0x82, 1, 2, 3, 4, 2, 0xea], + Ok(()), + ), + ( + Duration::from_secs(1), + None, + false, + vec![0x88, 0x80, 1, 2, 3, 4], + Ok(()), + ), + ( + Duration::from_secs(1), + Some(1000), + true, + vec![0x81, 0x82, 1, 2, 3, 4, 0x7a, 0x7f], + Err(WebDriverBiDiWebSocketFrameError::MalformedFrame { + reason: REUSED_CLIENT_MASK_KEY_REASON, + }), + ), ] { let listener = TcpListener::bind("127.0.0.1:0").expect("bind peer"); let address = listener.local_addr().expect("peer address"); @@ -851,6 +893,11 @@ mod tests { request.push(byte[0]); } stream.write_all(b"HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=\r\n\r\n").expect("opening response"); + let mut received = vec![0; expected_bytes.len()]; + stream + .read_exact(&mut received) + .expect("expected client frame"); + assert_eq!(received, expected_bytes); let mut byte = [0]; assert_eq!( TcpStream::read(&mut stream, &mut byte).expect("client EOF"), @@ -878,23 +925,25 @@ mod tests { .expect("write opening") .read_opening_response(Duration::from_secs(1)) .expect("read opening"); - let error = established + let established = if seed_text { + established + .write_text_frame( + "{}", + WebDriverBiDiWebSocketMaskKey::new([1, 2, 3, 4]), + timeout, + ) + .expect("seed text frame") + } else { + established + }; + let result = established .write_close_frame( - Some(1000), + status, WebDriverBiDiWebSocketMaskKey::new([1, 2, 3, 4]), timeout, ) - .expect_err("reject deadline"); - assert_eq!( - format!("{error:?}"), - format!( - "{:?}", - WebDriverBiDiWebSocketFrameError::InvalidFrameTimeout { - frame_timeout: timeout, - maximum_timeout: MAX_WEBSOCKET_FRAME_TIMEOUT, - } - ) - ); + .map(drop); + assert_eq!(format!("{result:?}"), format!("{expected_result:?}")); peer.join().expect("peer completed"); } } From 07ef43ec71b6dbd8540629bf1df5a63b81541ee4 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 8 Sep 2026 11:29:57 +0900 Subject: [PATCH 091/105] test(network): synchronize mask rejection peers Reproduce hidden peer failures, preserve exact mask-reuse errors, validate literal prior frames and require clean EOF without rejected response bytes. Reuse the opening-handshake fixture and keep production behavior unchanged. Co-Authored-By: OpenAI Codex --- AGENTS.md | 2 + CHANGELOG.md | 1 + ...webdriver_bidi_transport_close_evidence.rs | 80 +++++++++++++++---- 3 files changed, 68 insertions(+), 15 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index a421fe3f6..eaf66d187 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -121,6 +121,8 @@ Coordinate union alone does not reproduce LLVM's region summary: `RegionCoverage Inspect uncovered coordinates inside test assertions too: guarded `matches!` expressions can contribute never-taken failure branches to the file summary. Preserve exact variant and field checks rather than broadening the accepted error to make coverage pass. +Mask-reuse socket tests must consume the preceding text/Pong frame before sending the frame that triggers rejection. Assert the exact reuse error, literal preceding bytes, and EOF with no rejected response; propagate peer thread errors. A broad transport-error assertion plus an ignored join can pass because the peer rejected the fixture's own wrong opcode. An intervening fresh-key Pong also changes the adjacent-key history, so an older text key does not test adjacent Close-key reuse. + ## Release contract A release requires all current-head checks, complete coverage and docs, updated `CHANGELOG.md`, SBOM and provenance, reproducible artifacts, compatibility evidence, security review, and an explicit version decision. Pre-alpha commits are not releases. diff --git a/CHANGELOG.md b/CHANGELOG.md index 341404065..f3a32e972 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ All notable changes to OriginWeave are documented in this file. The format follo - Restored the simple frame-timeout validation after correcting coverage diagnosis; direct Close-writer tests verify invalid deadlines send no bytes and return the expected error. - Kept exact invalid-deadline error checks without compound test-only guards in the coverage measurement. - Verified literal masked Close bytes with and without a status code, and that a reused masking key emits no Close bytes after the preceding text frame. +- Corrected three masking-rejection test peers so unrelated socket errors cannot masquerade as key-reuse protection; each now verifies the preceding frame, the exact rejection, and no subsequent bytes. - Removed an unused private correlated-response accessor while retaining connection-generation validation at the receiving-message boundary, and corrected the Rust `AtomicU64` standard-library reference to its canonical type-alias page. - Integrated the current teardown prerequisites into transport-closure observation, including the previously uncollected release-record check, while retaining the unresolved connection-provenance finding and its downstream repair ownership. diff --git a/crates/originweave-network/tests/webdriver_bidi_transport_close_evidence.rs b/crates/originweave-network/tests/webdriver_bidi_transport_close_evidence.rs index 2a93af8fa..b2ac83fa5 100644 --- a/crates/originweave-network/tests/webdriver_bidi_transport_close_evidence.rs +++ b/crates/originweave-network/tests/webdriver_bidi_transport_close_evidence.rs @@ -70,6 +70,17 @@ fn read_masked_control_frame(stream: &mut TcpStream, expected_opcode: u8) -> io: fn established_with_server_frame( frame: Option<&'static [u8]>, +) -> Result> { + established_with_peer_script(move |stream| { + if let Some(frame) = frame { + stream.write_all(frame)?; + } + Ok(()) + }) +} + +fn established_with_peer_script( + script: impl FnOnce(&mut TcpStream) -> io::Result<()> + Send + 'static, ) -> Result> { let listener = TcpListener::bind(("127.0.0.1", 0))?; let local_addr = listener.local_addr()?; @@ -77,10 +88,7 @@ fn established_with_server_frame( let (mut stream, _) = listener.accept()?; read_opening_request(&mut stream)?; stream.write_all(OPENING_RESPONSE)?; - if let Some(frame) = frame { - stream.write_all(frame)?; - } - Ok(()) + script(&mut stream) }); let endpoint = format!("ws://{local_addr}/session/{SESSION_ID}"); @@ -536,7 +544,15 @@ fn malformed_close_frame_remains_a_typed_frame_failure() -> Result<(), Box Result<(), Box> { - let (established, server) = established_with_ping_then_held_open_peer_close()?; + let (established, server) = established_with_peer_script(|stream| { + let mut seed = [0; 8]; + stream.read_exact(&mut seed)?; + assert_eq!(seed, [0x81, 0x82, 5, 6, 7, 8, 0x7e, 0x7b]); + stream.write_all(&[0x89, 0x01, b'p'])?; + let mut byte = [0]; + assert_eq!(stream.read(&mut byte)?, 0); + Ok(()) + })?; let reused = WebDriverBiDiWebSocketMaskKey::new(PONG_MASK_KEY); let established = established.write_text_frame("{}", reused, Duration::from_millis(500))?; @@ -548,36 +564,64 @@ fn reused_pong_mask_key_preserves_typed_frame_failure() -> Result<(), Box Result<(), Box> { - let (established, server) = established_with_ping_then_held_open_peer_close()?; + let (established, server) = established_with_peer_script(|stream| { + stream.write_all(&[0x89, 0x01, b'p'])?; + let mut pong = [0; 7]; + stream.read_exact(&mut pong)?; + assert_eq!(pong, [0x8a, 0x81, 9, 10, 11, 12, 0x79]); + stream.write_all(&[0x88, 0x02, 0x03, 0xe8])?; + let mut byte = [0]; + assert_eq!(stream.read(&mut byte)?, 0); + Ok(()) + })?; let reused = WebDriverBiDiWebSocketMaskKey::new(CLOSE_MASK_KEY); - let established = established.write_text_frame("{}", reused, Duration::from_millis(500))?; let result = WebDriverBiDiWebSocketTransportClosureObservation::observe( established, - WebDriverBiDiWebSocketMaskKey::new(PONG_MASK_KEY), + reused, reused, Duration::from_millis(500), ); assert!(matches!( result, - Err(WebDriverBiDiWebSocketTransportClosureError::Frame { .. }) + Err(WebDriverBiDiWebSocketTransportClosureError::Frame { + source: WebDriverBiDiWebSocketFrameError::MalformedFrame { + reason: "client masking key was reused for consecutive frames on this established WebSocket" + } + }) )); - let _ = server.join(); + server + .join() + .map_err(|_| io::Error::other("mask-reuse peer panicked"))??; Ok(()) } #[test] fn reused_close_mask_key_on_peer_close_preserves_typed_frame_failure() -> Result<(), Box> { - let (established, server) = established_with_server_frame(Some(&[0x88, 0x02, 0x03, 0xe8]))?; + let (established, server) = established_with_peer_script(|stream| { + let mut seed = [0; 8]; + stream.read_exact(&mut seed)?; + assert_eq!(seed, [0x81, 0x82, 9, 10, 11, 12, 0x72, 0x77]); + stream.write_all(&[0x88, 0x02, 0x03, 0xe8])?; + let mut byte = [0]; + assert_eq!(stream.read(&mut byte)?, 0); + Ok(()) + })?; let reused = WebDriverBiDiWebSocketMaskKey::new(CLOSE_MASK_KEY); let established = established.write_text_frame("{}", reused, Duration::from_millis(500))?; @@ -589,8 +633,14 @@ fn reused_close_mask_key_on_peer_close_preserves_typed_frame_failure() -> Result ); assert!(matches!( result, - Err(WebDriverBiDiWebSocketTransportClosureError::Frame { .. }) + Err(WebDriverBiDiWebSocketTransportClosureError::Frame { + source: WebDriverBiDiWebSocketFrameError::MalformedFrame { + reason: "client masking key was reused for consecutive frames on this established WebSocket" + } + }) )); - let _ = server.join(); + server + .join() + .map_err(|_| io::Error::other("mask-reuse peer panicked"))??; Ok(()) } From f8df02e759b165191da55dd25ff5a70432cf123e Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 8 Sep 2026 12:05:13 +0900 Subject: [PATCH 092/105] fix(network): bound the complete closure exchange deadline Reproduce cumulative Close and EOF delay exceeding the caller budget, share one monotonic expiry across reads and writes, and reject late final evidence. Preserve timeout validation and exercise all deadline transitions with a controlled clock plus real loopback I/O. Co-Authored-By: OpenAI Codex --- AGENTS.md | 2 + CHANGELOG.md | 2 + .../src/webdriver_bidi_websocket_frame.rs | 4 +- ...driver_bidi_websocket_transport_closure.rs | 279 ++++++++++++++++-- ...webdriver_bidi_transport_close_evidence.rs | 25 ++ docs/doctoring.md | 10 + 6 files changed, 297 insertions(+), 25 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index eaf66d187..c64e54358 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -115,6 +115,8 @@ A skipped security, GPU, browser, TLS, or statistical test is not passing eviden ### Coverage diagnosis lesson +For a multi-step socket deadline, reproduce a sequence whose individual waits fit the limit but whose sum does not. Carry one monotonic expiry through every step and recheck before admitting final evidence. Pair real delayed-peer tests with a controlled clock at each read/write/evidence transition; a pre-I/O check alone cannot reject late completion. This is not a hard real-time host scheduling guarantee. + Aggregate LLVM code regions by source coordinates across function instantiations before identifying a missing path. An invalid-input test at a public entry point may stop at an earlier guard; it does not prove a later private writer's error return executed. Exercise that writer directly, assert the exact error, and verify the peer received no bytes. Do not rewrite production predicates based only on a file-level coverage deficit. Coordinate union alone does not reproduce LLVM's region summary: `RegionCoverageInfo::merge` takes maximum covered/total counts across instantiations. Complementary unit-test and integration-test executions can therefore leave a deficit. Exercise successful status-bearing and empty Close writes, invalid deadlines, and adjacent masking-key rejection in the same unit-test binary; compare literal wire bytes and join the peer. Reference: LLVM Project. (n.d.). *CoverageSummaryInfo.h* [Source code]. https://github.com/llvm/llvm-project/blob/main/llvm/tools/llvm-cov/CoverageSummaryInfo.h diff --git a/CHANGELOG.md b/CHANGELOG.md index f3a32e972..2384d45c0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,8 @@ All notable changes to OriginWeave are documented in this file. The format follo ### Changed +- Browser transport shutdown now shares one time budget across control replies and final connection closure; late closure cannot become successful completion evidence. + - Restored the simple frame-timeout validation after correcting coverage diagnosis; direct Close-writer tests verify invalid deadlines send no bytes and return the expected error. - Kept exact invalid-deadline error checks without compound test-only guards in the coverage measurement. - Verified literal masked Close bytes with and without a status code, and that a reused masking key emits no Close bytes after the preceding text frame. diff --git a/crates/originweave-network/src/webdriver_bidi_websocket_frame.rs b/crates/originweave-network/src/webdriver_bidi_websocket_frame.rs index 3f5c6f717..928e1a422 100644 --- a/crates/originweave-network/src/webdriver_bidi_websocket_frame.rs +++ b/crates/originweave-network/src/webdriver_bidi_websocket_frame.rs @@ -492,7 +492,9 @@ impl Error for WebDriverBiDiWebSocketFrameError { } } -fn validate_frame_timeout(frame_timeout: Duration) -> Result<(), WebDriverBiDiWebSocketFrameError> { +pub(crate) fn validate_frame_timeout( + frame_timeout: Duration, +) -> Result<(), WebDriverBiDiWebSocketFrameError> { if frame_timeout.is_zero() || frame_timeout > MAX_WEBSOCKET_FRAME_TIMEOUT { return Err(WebDriverBiDiWebSocketFrameError::InvalidFrameTimeout { frame_timeout, diff --git a/crates/originweave-network/src/webdriver_bidi_websocket_transport_closure.rs b/crates/originweave-network/src/webdriver_bidi_websocket_transport_closure.rs index bb5a1f0fa..48ab4dfca 100644 --- a/crates/originweave-network/src/webdriver_bidi_websocket_transport_closure.rs +++ b/crates/originweave-network/src/webdriver_bidi_websocket_transport_closure.rs @@ -1,10 +1,29 @@ -use std::{error::Error, fmt, time::Duration}; +use std::{ + error::Error, + fmt, + time::{Duration, Instant}, +}; use crate::{ WebDriverBiDiWebSocketEstablished, WebDriverBiDiWebSocketFrameError, WebDriverBiDiWebSocketMaskKey, webdriver_bidi_connection::WebDriverBiDiConnectionGeneration, }; +struct ClosureDeadline<'a> { + expires_at: Instant, + now: &'a mut dyn FnMut() -> Instant, +} + +impl ClosureDeadline<'_> { + fn remaining(&mut self) -> Result { + let remaining = self.expires_at.saturating_duration_since((self.now)()); + if remaining.is_zero() { + return Err(WebDriverBiDiWebSocketTransportClosureError::DeadlineExpired); + } + Ok(remaining) + } +} + /// Bounded transport-closure condition observed on one consumed WebDriver BiDi WebSocket. /// /// Every successful variant proves clean TCP EOF on the exact established connection. A preceding @@ -42,20 +61,42 @@ impl WebDriverBiDiWebSocketTransportClosureObservation { /// the required Close response. At most one pre-Close Ping or unsolicited Pong is admitted; /// additional control traffic, application data, timeout, partial EOF, malformed frames, write /// failures, or any post-Close frame fail closed. Peer Close alone is never success: after the - /// masked response this boundary requires zero-byte TCP EOF within the same bounded frame - /// deadline before producing closure evidence. + /// masked response this boundary requires zero-byte TCP EOF within one operation-wide + /// deadline covering all reads and writes. Evidence arriving after that deadline is rejected. pub fn observe( established: WebDriverBiDiWebSocketEstablished, pong_masking_key: WebDriverBiDiWebSocketMaskKey, close_masking_key: WebDriverBiDiWebSocketMaskKey, frame_timeout: Duration, ) -> Result { + Self::observe_with_clock( + established, + pong_masking_key, + close_masking_key, + frame_timeout, + &mut Instant::now, + ) + } + + fn observe_with_clock( + established: WebDriverBiDiWebSocketEstablished, + pong_masking_key: WebDriverBiDiWebSocketMaskKey, + close_masking_key: WebDriverBiDiWebSocketMaskKey, + frame_timeout: Duration, + now: &mut dyn FnMut() -> Instant, + ) -> Result { + crate::webdriver_bidi_websocket_frame::validate_frame_timeout(frame_timeout) + .map_err(|source| WebDriverBiDiWebSocketTransportClosureError::Frame { source })?; + let mut deadline = ClosureDeadline { + expires_at: now() + frame_timeout, + now, + }; let connection_generation = established.transport_evidence().connection_generation(); Self::observe_pre_close( established, pong_masking_key, close_masking_key, - frame_timeout, + &mut deadline, true, connection_generation, ) @@ -65,24 +106,24 @@ impl WebDriverBiDiWebSocketTransportClosureObservation { established: WebDriverBiDiWebSocketEstablished, pong_masking_key: WebDriverBiDiWebSocketMaskKey, close_masking_key: WebDriverBiDiWebSocketMaskKey, - frame_timeout: Duration, + deadline: &mut ClosureDeadline<'_>, allow_pre_close_control: bool, connection_generation: WebDriverBiDiConnectionGeneration, ) -> Result { - match established.read_frame(frame_timeout) { + match established.read_frame(deadline.remaining()?) { Ok((established, frame)) if frame.opcode() == 0xa && allow_pre_close_control => { Self::observe_pre_close( established, pong_masking_key, close_masking_key, - frame_timeout, + deadline, false, connection_generation, ) } Ok((established, frame)) if frame.opcode() == 0x9 && allow_pre_close_control => { let established = established - .write_pong_frame(frame.payload(), pong_masking_key, frame_timeout) + .write_pong_frame(frame.payload(), pong_masking_key, deadline.remaining()?) .map_err( |source| WebDriverBiDiWebSocketTransportClosureError::Frame { source }, )?; @@ -90,7 +131,7 @@ impl WebDriverBiDiWebSocketTransportClosureObservation { established, pong_masking_key, close_masking_key, - frame_timeout, + deadline, false, connection_generation, ) @@ -101,13 +142,17 @@ impl WebDriverBiDiWebSocketTransportClosureObservation { .get(..2) .map(|bytes| u16::from_be_bytes([bytes[0], bytes[1]])); let established = established - .write_close_frame(peer_close_status_code, close_masking_key, frame_timeout) + .write_close_frame( + peer_close_status_code, + close_masking_key, + deadline.remaining()?, + ) .map_err( |source| WebDriverBiDiWebSocketTransportClosureError::Frame { source }, )?; Self::observe_eof_after_close( established, - frame_timeout, + deadline, peer_close_status_code, connection_generation, ) @@ -117,27 +162,33 @@ impl WebDriverBiDiWebSocketTransportClosureObservation { opcode: frame.opcode(), }, ), - Err(WebDriverBiDiWebSocketFrameError::FrameEnded { bytes_read: 0 }) => Ok(Self { - kind: WebDriverBiDiWebSocketTransportClosureKind::PeerEof, - peer_close_status_code: None, - connection_generation, - }), + Err(WebDriverBiDiWebSocketFrameError::FrameEnded { bytes_read: 0 }) => { + deadline.remaining()?; + Ok(Self { + kind: WebDriverBiDiWebSocketTransportClosureKind::PeerEof, + peer_close_status_code: None, + connection_generation, + }) + } Err(source) => Err(WebDriverBiDiWebSocketTransportClosureError::Frame { source }), } } fn observe_eof_after_close( established: WebDriverBiDiWebSocketEstablished, - frame_timeout: Duration, + deadline: &mut ClosureDeadline<'_>, peer_close_status_code: Option, connection_generation: WebDriverBiDiConnectionGeneration, ) -> Result { - match established.read_frame(frame_timeout) { - Err(WebDriverBiDiWebSocketFrameError::FrameEnded { bytes_read: 0 }) => Ok(Self { - kind: WebDriverBiDiWebSocketTransportClosureKind::PeerCloseThenEof, - peer_close_status_code, - connection_generation, - }), + match established.read_frame(deadline.remaining()?) { + Err(WebDriverBiDiWebSocketFrameError::FrameEnded { bytes_read: 0 }) => { + deadline.remaining()?; + Ok(Self { + kind: WebDriverBiDiWebSocketTransportClosureKind::PeerCloseThenEof, + peer_close_status_code, + connection_generation, + }) + } Ok((_established, frame)) => Err( WebDriverBiDiWebSocketTransportClosureError::UnexpectedFrame { opcode: frame.opcode(), @@ -167,6 +218,8 @@ impl WebDriverBiDiWebSocketTransportClosureObservation { /// Fail-closed errors while converting one established BiDi transport into closure evidence. #[derive(Debug)] pub enum WebDriverBiDiWebSocketTransportClosureError { + /// The operation-wide deadline expired before transport-closure evidence was admitted. + DeadlineExpired, /// The peer sent a valid WebSocket frame outside the bounded closing state machine. UnexpectedFrame { /// Exact validated RFC 6455 opcode observed instead of admissible bounded closing traffic. @@ -182,6 +235,9 @@ pub enum WebDriverBiDiWebSocketTransportClosureError { impl fmt::Display for WebDriverBiDiWebSocketTransportClosureError { fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { match self { + Self::DeadlineExpired => { + formatter.write_str("WebDriver BiDi transport closure deadline expired") + } Self::UnexpectedFrame { .. } => formatter .write_str("WebDriver BiDi peer sent non-closure traffic instead of closing"), Self::Frame { .. } => { @@ -194,8 +250,183 @@ impl fmt::Display for WebDriverBiDiWebSocketTransportClosureError { impl Error for WebDriverBiDiWebSocketTransportClosureError { fn source(&self) -> Option<&(dyn Error + 'static)> { match self { - Self::UnexpectedFrame { .. } => None, + Self::UnexpectedFrame { .. } | Self::DeadlineExpired => None, Self::Frame { source } => Some(source), } } } + +#[cfg(test)] +#[allow(clippy::expect_used)] +mod tests { + use super::*; + use crate::{ + WebDriverBiDiTcpConnectionPlan, WebDriverBiDiWebSocketClientKey, + WebDriverBiDiWebSocketHandshakePlan, + }; + use originweave_core::WebDriverBiDiWebSocketEndpoint; + use std::{ + io::{Read, Write}, + net::{Shutdown, TcpListener, TcpStream}, + thread, + }; + + fn established_peer(frames: &[u8]) -> (WebDriverBiDiWebSocketEstablished, TcpStream) { + let listener = TcpListener::bind("127.0.0.1:0").expect("bind peer"); + let address = listener.local_addr().expect("peer address"); + let peer = thread::spawn(move || { + let (mut stream, _) = listener.accept().expect("accept client"); + stream + .set_read_timeout(Some(Duration::from_secs(2))) + .expect("bound peer"); + let mut request = Vec::new(); + while !request.ends_with(b"\r\n\r\n") { + let mut byte = [0]; + stream.read_exact(&mut byte).expect("opening request"); + request.push(byte[0]); + } + stream.write_all(b"HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=\r\n\r\n").expect("opening response"); + stream + }); + let session = "01234567-89ab-cdef-0123-456789abcdef"; + let endpoint = + WebDriverBiDiWebSocketEndpoint::new(&format!("ws://{address}/session/{session}")) + .expect("endpoint"); + let target = endpoint + .correlate_session_id(session) + .expect("session") + .into_explicit_connect_target() + .expect("target"); + let connection = WebDriverBiDiTcpConnectionPlan::new(target, Duration::from_secs(1), 1) + .expect("plan") + .connect() + .expect("connect"); + let key = WebDriverBiDiWebSocketClientKey::new("dGhlIHNhbXBsZSBub25jZQ==").expect("key"); + let established = WebDriverBiDiWebSocketHandshakePlan::new(connection, key) + .expect("handshake") + .write_opening_request(Duration::from_secs(1)) + .expect("opening write") + .read_opening_response(Duration::from_secs(1)) + .expect("opening read"); + let mut peer = peer.join().expect("peer handshake completed"); + peer.write_all(frames).expect("peer frames"); + peer.shutdown(Shutdown::Write).expect("peer write EOF"); + (established, peer) + } + + #[test] + fn fixed_clock_accepts_complete_closure_before_deadline() { + for (frames, kind, status) in [ + ( + &[][..], + WebDriverBiDiWebSocketTransportClosureKind::PeerEof, + None, + ), + ( + &[0x88, 0][..], + WebDriverBiDiWebSocketTransportClosureKind::PeerCloseThenEof, + None, + ), + ( + &[0x88, 2, 3, 0xe8][..], + WebDriverBiDiWebSocketTransportClosureKind::PeerCloseThenEof, + Some(1000), + ), + ( + &[0x89, 0, 0x88, 0][..], + WebDriverBiDiWebSocketTransportClosureKind::PeerCloseThenEof, + None, + ), + ( + &[0x8a, 0, 0x88, 0][..], + WebDriverBiDiWebSocketTransportClosureKind::PeerCloseThenEof, + None, + ), + ] { + let (established, _peer) = established_peer(frames); + let now = Instant::now(); + let observation = + WebDriverBiDiWebSocketTransportClosureObservation::observe_with_clock( + established, + WebDriverBiDiWebSocketMaskKey::new([1, 2, 3, 4]), + WebDriverBiDiWebSocketMaskKey::new([5, 6, 7, 8]), + Duration::from_secs(1), + &mut || now, + ) + .expect("closure before deadline"); + assert_eq!(observation.kind(), kind); + assert_eq!(observation.peer_close_status_code(), status); + } + } + + #[test] + fn invalid_deadline_is_rejected_before_clock_use() { + for timeout in [ + Duration::ZERO, + crate::MAX_WEBSOCKET_FRAME_TIMEOUT + Duration::from_nanos(1), + ] { + let (established, _peer) = established_peer(&[]); + let mut clock_calls = 0; + let result = WebDriverBiDiWebSocketTransportClosureObservation::observe_with_clock( + established, + WebDriverBiDiWebSocketMaskKey::new([1, 2, 3, 4]), + WebDriverBiDiWebSocketMaskKey::new([5, 6, 7, 8]), + timeout, + &mut || { + clock_calls += 1; + Instant::now() + }, + ); + assert!(matches!( + result, + Err(WebDriverBiDiWebSocketTransportClosureError::Frame { + source: WebDriverBiDiWebSocketFrameError::InvalidFrameTimeout { .. } + }) + )); + assert_eq!(clock_calls, 0); + } + } + + #[test] + fn deadline_expiry_is_checked_at_every_closure_transition() { + for (frames, expire_tick) in [ + (&[][..], 2), + (&[0x88, 0][..], 1), + (&[0x88, 0][..], 2), + (&[0x88, 0][..], 3), + (&[0x88, 0][..], 4), + (&[0x89, 0, 0x88, 0][..], 2), + (&[0x89, 0, 0x88, 0][..], 3), + (&[0x89, 0, 0x88, 0][..], 4), + (&[0x89, 0, 0x88, 0][..], 5), + (&[0x89, 0, 0x88, 0][..], 6), + (&[0x8a, 0, 0x88, 0][..], 2), + ] { + let (established, _peer) = established_peer(frames); + let start = Instant::now(); + let mut tick = 0; + let mut now = || { + let elapsed = Duration::from_secs(u64::from(tick >= expire_tick)); + tick += 1; + start + elapsed + }; + let error = WebDriverBiDiWebSocketTransportClosureObservation::observe_with_clock( + established, + WebDriverBiDiWebSocketMaskKey::new([1, 2, 3, 4]), + WebDriverBiDiWebSocketMaskKey::new([5, 6, 7, 8]), + Duration::from_secs(1), + &mut now, + ) + .expect_err("expired closure must not produce evidence"); + assert!(matches!( + error, + WebDriverBiDiWebSocketTransportClosureError::DeadlineExpired + )); + assert_eq!( + error.to_string(), + "WebDriver BiDi transport closure deadline expired" + ); + assert!(error.source().is_none()); + } + } +} diff --git a/crates/originweave-network/tests/webdriver_bidi_transport_close_evidence.rs b/crates/originweave-network/tests/webdriver_bidi_transport_close_evidence.rs index b2ac83fa5..8b1aa3ac9 100644 --- a/crates/originweave-network/tests/webdriver_bidi_transport_close_evidence.rs +++ b/crates/originweave-network/tests/webdriver_bidi_transport_close_evidence.rs @@ -289,6 +289,31 @@ fn observe( ) } +#[test] +fn close_exchange_cannot_restart_the_operation_deadline() -> Result<(), Box> { + let (start_sender, start_receiver) = std::sync::mpsc::channel(); + let (established, server) = established_with_peer_script(move |stream| { + start_receiver.recv().map_err(io::Error::other)?; + thread::sleep(Duration::from_millis(750)); + stream.write_all(&[0x88, 0x02, 0x03, 0xe8])?; + assert_eq!(read_masked_control_frame(stream, 0x08)?, [0x03, 0xe8]); + thread::sleep(Duration::from_millis(750)); + Ok(()) + })?; + start_sender.send(())?; + let result = observe(established, Duration::from_secs(1)); + server + .join() + .map_err(|_| io::Error::other("deadline peer panicked"))??; + assert!(matches!( + result, + Err(WebDriverBiDiWebSocketTransportClosureError::Frame { + source: WebDriverBiDiWebSocketFrameError::FrameReadTimedOut { bytes_read: 0, .. } + }) + )); + Ok(()) +} + #[test] fn ping_before_close_requires_masked_pong_and_still_waits_for_tcp_closure() -> Result<(), Box> { diff --git a/docs/doctoring.md b/docs/doctoring.md index cc9844d74..39575c9da 100644 --- a/docs/doctoring.md +++ b/docs/doctoring.md @@ -18,6 +18,16 @@ UTS #39 Revision 32 is the current Unicode security-mechanisms standard and mark RFC 6455 carries the WebSocket opening handshake over HTTP/1.1, and RFC 9110 permits `obs-text` octets (`%x80-FF`) in field values while retaining ASCII field-name and delimiter syntax. RFC 6455 also specifies that unknown opening-handshake header fields are ignored. OriginWeave therefore treats unknown extension-field values as opaque compatibility data rather than requiring the entire opening response to be UTF-8, while keeping the authority-bearing `Upgrade`, `Connection`, and `Sec-WebSocket-Accept` checks fail closed: opaque replacement material cannot satisfy the reviewed ASCII token or exact accept-value contracts. Ignoring an unknown field never grants browser, network, secret, approval, or Agent authority. RFC 6455 section 5.3 additionally requires every client-to-server frame to use a fresh unpredictable 32-bit masking key derived from strong entropy. OriginWeave's frame owner enforces that normative masking requirement and also rejects immediate reuse of the preceding key as a local fail-closed stuck-randomness defense; the adjacent-reuse rule is stronger local policy, not an RFC 6455 requirement. +### Transport closure deadline + +The experimental BiDi transport-closure observer applies one local operation-wide +deadline across pre-Close control traffic, the masked Close response, and final TCP +EOF. This is an OriginWeave resource policy, not an RFC 6455 timeout value. Each I/O +step receives only the remaining budget, and final closure evidence is rejected if +the monotonic deadline has expired. It prevents per-frame budget renewal without +claiming a hard real-time bound on host scheduling or proving process exit/profile +cleanup. Tests combine a delayed real peer with deterministic clock transitions. + ### Browser origin equivalence The WHATWG URL host parser and Chromium canonicalizer classify shortened decimal, integer, hexadecimal, legacy octal-looking, and mixed-component numeric hosts as IPv4 or broken IPv4 candidates rather than ordinary DNS names. Chromium's regression suite includes values such as `192`, `0xC0a80001`, `030052000001`, and mixed hexadecimal components. A non-final empty `0x` component can participate in Chromium's multi-part IPv4 truncation behavior, but a final `0x` label does not produce an IPv4 number because stripping its prefix leaves no digits; it remains a domain label. Chromium also warns that broken IP-like hosts must not be connected because another resolver could accept them. OriginWeave therefore admits only canonical dotted-decimal IPv4 into its policy origin type, rejects browser-special numeric spellings before DNS validation, and preserves final non-numeric DNS labels such as `0x`. From 292d73bb2b5ff773349fd643139c6b503c1aca88 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 8 Sep 2026 12:15:10 +0900 Subject: [PATCH 093/105] test(network): verify deadline and protocol paths together Exercise valid and invalid clock admission, complete closure, precise protocol failures and all expiry transitions in the same unit binary. Preserve the full coverage gate and avoid an intentionally never-invoked test clock. Co-Authored-By: OpenAI Codex --- ...driver_bidi_websocket_transport_closure.rs | 115 ++++++++++++++++-- 1 file changed, 103 insertions(+), 12 deletions(-) diff --git a/crates/originweave-network/src/webdriver_bidi_websocket_transport_closure.rs b/crates/originweave-network/src/webdriver_bidi_websocket_transport_closure.rs index 48ab4dfca..aed676243 100644 --- a/crates/originweave-network/src/webdriver_bidi_websocket_transport_closure.rs +++ b/crates/originweave-network/src/webdriver_bidi_websocket_transport_closure.rs @@ -361,9 +361,13 @@ mod tests { #[test] fn invalid_deadline_is_rejected_before_clock_use() { - for timeout in [ - Duration::ZERO, - crate::MAX_WEBSOCKET_FRAME_TIMEOUT + Duration::from_nanos(1), + for (timeout, expected_calls) in [ + (Duration::ZERO, 0), + ( + crate::MAX_WEBSOCKET_FRAME_TIMEOUT + Duration::from_nanos(1), + 0, + ), + (Duration::from_secs(1), 3), ] { let (established, _peer) = established_peer(&[]); let mut clock_calls = 0; @@ -377,13 +381,18 @@ mod tests { Instant::now() }, ); - assert!(matches!( - result, + let expected = if expected_calls == 0 { Err(WebDriverBiDiWebSocketTransportClosureError::Frame { - source: WebDriverBiDiWebSocketFrameError::InvalidFrameTimeout { .. } + source: WebDriverBiDiWebSocketFrameError::InvalidFrameTimeout { + frame_timeout: timeout, + maximum_timeout: crate::MAX_WEBSOCKET_FRAME_TIMEOUT, + }, }) - )); - assert_eq!(clock_calls, 0); + } else { + Ok(()) + }; + assert_eq!(format!("{:?}", result.map(|_| ())), format!("{expected:?}")); + assert_eq!(clock_calls, expected_calls); } } @@ -418,10 +427,7 @@ mod tests { &mut now, ) .expect_err("expired closure must not produce evidence"); - assert!(matches!( - error, - WebDriverBiDiWebSocketTransportClosureError::DeadlineExpired - )); + assert_eq!(format!("{error:?}"), "DeadlineExpired"); assert_eq!( error.to_string(), "WebDriver BiDi transport closure deadline expired" @@ -429,4 +435,89 @@ mod tests { assert!(error.source().is_none()); } } + + #[test] + fn fixed_deadline_preserves_protocol_and_masking_failures() { + use WebDriverBiDiWebSocketFrameError::{FrameEnded, MalformedFrame}; + use WebDriverBiDiWebSocketTransportClosureError::{Frame, UnexpectedFrame}; + let reused = + "client masking key was reused for consecutive frames on this established WebSocket"; + for (frames, seed_text, pong_key, close_key, expected) in [ + ( + &[0x81, 0][..], + false, + [1, 2, 3, 4], + [5, 6, 7, 8], + UnexpectedFrame { opcode: 1 }, + ), + ( + &[0x88][..], + false, + [1, 2, 3, 4], + [5, 6, 7, 8], + Frame { + source: FrameEnded { bytes_read: 1 }, + }, + ), + ( + &[0x88, 0, 0x8a, 0][..], + false, + [1, 2, 3, 4], + [5, 6, 7, 8], + UnexpectedFrame { opcode: 0xa }, + ), + ( + &[0x88, 0, 0x88][..], + false, + [1, 2, 3, 4], + [5, 6, 7, 8], + Frame { + source: FrameEnded { bytes_read: 1 }, + }, + ), + ( + &[0x89, 0][..], + true, + [1, 2, 3, 4], + [5, 6, 7, 8], + Frame { + source: MalformedFrame { reason: reused }, + }, + ), + ( + &[0x88, 0][..], + true, + [5, 6, 7, 8], + [1, 2, 3, 4], + Frame { + source: MalformedFrame { reason: reused }, + }, + ), + ] { + let (established, _peer) = established_peer(frames); + let established = if seed_text { + established + .write_text_frame( + "{}", + WebDriverBiDiWebSocketMaskKey::new([1, 2, 3, 4]), + Duration::from_secs(1), + ) + .expect("seed masking history") + } else { + established + }; + let now = Instant::now(); + let error = WebDriverBiDiWebSocketTransportClosureObservation::observe_with_clock( + established, + WebDriverBiDiWebSocketMaskKey::new(pong_key), + WebDriverBiDiWebSocketMaskKey::new(close_key), + Duration::from_secs(1), + &mut || now, + ) + .expect_err("protocol failure must survive the deadline wrapper"); + assert_eq!(format!("{error:?}"), format!("{expected:?}")); + assert_eq!(error.to_string(), expected.to_string()); + assert_eq!(error.source().is_some(), expected.source().is_some()); + } + } } From 8716b9d441960a112446c1f89ab417ad9abe28d2 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 8 Sep 2026 12:28:35 +0900 Subject: [PATCH 094/105] fix(network): reject unassigned protocol close codes Co-Authored-By: OpenAI Codex --- AGENTS.md | 2 ++ CHANGELOG.md | 2 ++ .../src/webdriver_bidi_websocket_frame.rs | 22 +++++++++++++++---- ...webdriver_bidi_transport_close_evidence.rs | 22 +++++++++++++++++++ docs/doctoring.md | 18 +++++++++++++++ 5 files changed, 62 insertions(+), 4 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index c64e54358..102c67d64 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -115,6 +115,8 @@ A skipped security, GPU, browser, TLS, or statistical test is not passing eviden ### Coverage diagnosis lesson +For Close-code validation, check the current IANA registry as well as RFC 6455: the protocol-reserved range is not an allowance for unassigned values. Keep application/private ranges separate, test assigned and reserved boundaries, and verify rejected peer codes produce neither an echo nor closure evidence. Record the registry date; future assignments need a reviewed update, not ambient network lookup during frame parsing. + For a multi-step socket deadline, reproduce a sequence whose individual waits fit the limit but whose sum does not. Carry one monotonic expiry through every step and recheck before admitting final evidence. Pair real delayed-peer tests with a controlled clock at each read/write/evidence transition; a pre-I/O check alone cannot reject late completion. This is not a hard real-time host scheduling guarantee. Aggregate LLVM code regions by source coordinates across function instantiations before identifying a missing path. An invalid-input test at a public entry point may stop at an earlier guard; it does not prove a later private writer's error return executed. Exercise that writer directly, assert the exact error, and verify the peer received no bytes. Do not rewrite production predicates based only on a file-level coverage deficit. diff --git a/CHANGELOG.md b/CHANGELOG.md index 2384d45c0..ee42b9a7b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,8 @@ All notable changes to OriginWeave are documented in this file. The format follo ### Changed +- Browser transport shutdown rejects unassigned protocol close codes before replying or recording completion; application and private-use code ranges remain supported. + - Browser transport shutdown now shares one time budget across control replies and final connection closure; late closure cannot become successful completion evidence. - Restored the simple frame-timeout validation after correcting coverage diagnosis; direct Close-writer tests verify invalid deadlines send no bytes and return the expected error. diff --git a/crates/originweave-network/src/webdriver_bidi_websocket_frame.rs b/crates/originweave-network/src/webdriver_bidi_websocket_frame.rs index 928e1a422..8a0ec303a 100644 --- a/crates/originweave-network/src/webdriver_bidi_websocket_frame.rs +++ b/crates/originweave-network/src/webdriver_bidi_websocket_frame.rs @@ -813,7 +813,7 @@ fn validate_close_frame( }); } let status_code = u16::from_be_bytes([frame.payload()[0], frame.payload()[1]]); - if !(1000..=4999).contains(&status_code) || matches!(status_code, 1004 | 1005 | 1006 | 1015) { + if !(1000..=4999).contains(&status_code) || matches!(status_code, 1004..=1006 | 1015..=2999) { return Err(WebDriverBiDiWebSocketFrameError::MalformedFrame { reason: "Close frame status code is not valid on the wire", }); @@ -1457,16 +1457,30 @@ mod tests { payload: vec![0x03, 0xe8, 0xff], }; assert!(validate_close_frame(&invalid_utf8).is_err()); - for status in [999_u16, 1004, 1005, 1006, 1015, 5000] { + for status in [ + 0_u16, + 999, + 1004, + 1005, + 1006, + 1015, + 1016, + 2000, + 2999, + 5000, + u16::MAX, + ] { let payload = status.to_be_bytes().to_vec(); let frame = WebDriverBiDiWebSocketFrame { fin: true, opcode: 8, payload, }; - assert!(validate_close_frame(&frame).is_err()); + assert!(validate_close_frame(&frame).is_err(), "status {status}"); } - for status in [1000_u16, 3000, 4000] { + for status in [ + 1000_u16, 1003, 1007, 1011, 1012, 1013, 1014, 3000, 3999, 4000, 4999, + ] { let mut payload = status.to_be_bytes().to_vec(); payload.extend_from_slice(b"ok"); let frame = WebDriverBiDiWebSocketFrame { diff --git a/crates/originweave-network/tests/webdriver_bidi_transport_close_evidence.rs b/crates/originweave-network/tests/webdriver_bidi_transport_close_evidence.rs index 8b1aa3ac9..5f9cab41a 100644 --- a/crates/originweave-network/tests/webdriver_bidi_transport_close_evidence.rs +++ b/crates/originweave-network/tests/webdriver_bidi_transport_close_evidence.rs @@ -541,6 +541,28 @@ fn application_frame_after_teardown_does_not_become_closure_evidence() -> Result Ok(()) } +#[test] +fn unassigned_protocol_close_codes_emit_no_reply_or_evidence() -> Result<(), Box> { + for status_code in [1016_u16, 2000, 2999] { + let (established, server) = established_with_peer_script(move |stream| { + let [high, low] = status_code.to_be_bytes(); + stream.write_all(&[0x88, 0x02, high, low])?; + let mut reply = [0_u8; 1]; + assert_eq!(stream.read(&mut reply)?, 0, "invalid Close was echoed"); + Ok(()) + })?; + let result = observe(established, Duration::from_millis(500)); + server + .join() + .map_err(|_| io::Error::other("reserved-code test server panicked"))??; + assert_eq!( + format!("{result:?}"), + "Err(Frame { source: MalformedFrame { reason: \"Close frame status code is not valid on the wire\" } })" + ); + } + Ok(()) +} + #[test] fn malformed_close_frame_remains_a_typed_frame_failure() -> Result<(), Box> { let (established, server) = established_with_server_frame(Some(&[0x88, 0x01, 0x00]))?; diff --git a/docs/doctoring.md b/docs/doctoring.md index 39575c9da..ee15a050a 100644 --- a/docs/doctoring.md +++ b/docs/doctoring.md @@ -28,6 +28,24 @@ the monotonic deadline has expired. It prevents per-frame budget renewal without claiming a hard real-time bound on host scheduling or proving process exit/profile cleanup. Tests combine a delayed real peer with deterministic clock transitions. +### WebSocket Close code admission + +RFC 6455 section 7.4.2 reserves 1000–2999 for protocol and extension definitions. +The IANA registry retrieved on 8 September 2026 lists 1016–2999 as unassigned; +1012, 1013, and 1014 are assigned. The current adapter therefore rejects the +unassigned protocol range alongside 1004–1006 and 1015, which cannot appear as +ordinary wire status codes. It retains 3000–3999 for application codes and +4000–4999 for private use without interpreting their meanings or treating them +as proof of successful browser work. This is a reviewed static admission policy, +not an online registry check. New protocol assignments require a reviewed update. +The shared frame validator runs before any Close echo or closure evidence. + +Internet Assigned Numbers Authority. (2026). *WebSocket protocol registries*. +Retrieved September 8, 2026, from https://www.iana.org/assignments/websocket + +Fette, I., & Melnikov, A. (2011). *The WebSocket protocol* (RFC 6455, § 7.4.2). +Internet Engineering Task Force. https://www.rfc-editor.org/rfc/rfc6455 + ### Browser origin equivalence The WHATWG URL host parser and Chromium canonicalizer classify shortened decimal, integer, hexadecimal, legacy octal-looking, and mixed-component numeric hosts as IPv4 or broken IPv4 candidates rather than ordinary DNS names. Chromium's regression suite includes values such as `192`, `0xC0a80001`, `030052000001`, and mixed hexadecimal components. A non-final empty `0x` component can participate in Chromium's multi-part IPv4 truncation behavior, but a final `0x` label does not produce an IPv4 number because stripping its prefix leaves no digits; it remains a domain label. Chromium also warns that broken IP-like hosts must not be connected because another resolver could accept them. OriginWeave therefore admits only canonical dotted-decimal IPv4 into its policy origin type, rejects browser-special numeric spellings before DNS validation, and preserves final non-numeric DNS labels such as `0x`. From 383f15767db7aa73ae7a70e6d3cc2346b3895321 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 8 Sep 2026 12:41:06 +0900 Subject: [PATCH 095/105] fix(network): admit bounded repeated closure controls Co-Authored-By: OpenAI Codex --- AGENTS.md | 2 + CHANGELOG.md | 2 + ...driver_bidi_websocket_transport_closure.rs | 143 ++++++++++++++---- ...driver_bidi_session_teardown_assessment.rs | 2 +- ...webdriver_bidi_transport_close_evidence.rs | 114 +++++++++++--- docs/doctoring.md | 11 ++ 6 files changed, 220 insertions(+), 54 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 102c67d64..b77c64961 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -115,6 +115,8 @@ A skipped security, GPU, browser, TLS, or statistical test is not passing eviden ### Coverage diagnosis lesson +Repeated control-frame support must preserve the total deadline and per-write masking authority. Supply a separate caller-owned key per Ping, consume none for unsolicited Pong, and keep the Close key independent. Test exact-budget success, budget overflow, exhausted keys, and reused keys with a peer that verifies no rejected reply. The 64-control budget is local resource policy, not an RFC limit. + For Close-code validation, check the current IANA registry as well as RFC 6455: the protocol-reserved range is not an allowance for unassigned values. Keep application/private ranges separate, test assigned and reserved boundaries, and verify rejected peer codes produce neither an echo nor closure evidence. Record the registry date; future assignments need a reviewed update, not ambient network lookup during frame parsing. For a multi-step socket deadline, reproduce a sequence whose individual waits fit the limit but whose sum does not. Carry one monotonic expiry through every step and recheck before admitting final evidence. Pair real delayed-peer tests with a controlled clock at each read/write/evidence transition; a pre-I/O check alone cannot reject late completion. This is not a hard real-time host scheduling guarantee. diff --git a/CHANGELOG.md b/CHANGELOG.md index ee42b9a7b..901630df8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,8 @@ All notable changes to OriginWeave are documented in this file. The format follo ### Changed +- Browser transport shutdown now handles repeated keepalive traffic within one time budget, using a separate supplied key for each reply and rejecting exhausted keys or excess traffic without reporting completion. + - Browser transport shutdown rejects unassigned protocol close codes before replying or recording completion; application and private-use code ranges remain supported. - Browser transport shutdown now shares one time budget across control replies and final connection closure; late closure cannot become successful completion evidence. diff --git a/crates/originweave-network/src/webdriver_bidi_websocket_transport_closure.rs b/crates/originweave-network/src/webdriver_bidi_websocket_transport_closure.rs index aed676243..9d038ac52 100644 --- a/crates/originweave-network/src/webdriver_bidi_websocket_transport_closure.rs +++ b/crates/originweave-network/src/webdriver_bidi_websocket_transport_closure.rs @@ -9,6 +9,8 @@ use crate::{ WebDriverBiDiWebSocketMaskKey, webdriver_bidi_connection::WebDriverBiDiConnectionGeneration, }; +const MAX_PRE_CLOSE_CONTROL_FRAMES: usize = 64; + struct ClosureDeadline<'a> { expires_at: Instant, now: &'a mut dyn FnMut() -> Instant, @@ -57,21 +59,22 @@ pub struct WebDriverBiDiWebSocketTransportClosureObservation { impl WebDriverBiDiWebSocketTransportClosureObservation { /// Consume one established connection and prove bounded transport closure. /// - /// The caller supplies independent fresh masking keys for the only pre-Close Ping response and - /// the required Close response. At most one pre-Close Ping or unsolicited Pong is admitted; - /// additional control traffic, application data, timeout, partial EOF, malformed frames, write + /// The caller supplies independent fresh masking keys for each pre-Close Ping response and + /// the required Close response. Up to 64 pre-Close Ping/Pong frames are admitted as a local + /// resource limit. Pings consume keys in slice order; unsolicited Pongs consume no key. + /// Exhausted keys, excess control traffic, application data, timeout, partial EOF, malformed frames, write /// failures, or any post-Close frame fail closed. Peer Close alone is never success: after the /// masked response this boundary requires zero-byte TCP EOF within one operation-wide /// deadline covering all reads and writes. Evidence arriving after that deadline is rejected. pub fn observe( established: WebDriverBiDiWebSocketEstablished, - pong_masking_key: WebDriverBiDiWebSocketMaskKey, + pong_masking_keys: &[WebDriverBiDiWebSocketMaskKey], close_masking_key: WebDriverBiDiWebSocketMaskKey, frame_timeout: Duration, ) -> Result { Self::observe_with_clock( established, - pong_masking_key, + pong_masking_keys, close_masking_key, frame_timeout, &mut Instant::now, @@ -80,7 +83,7 @@ impl WebDriverBiDiWebSocketTransportClosureObservation { fn observe_with_clock( established: WebDriverBiDiWebSocketEstablished, - pong_masking_key: WebDriverBiDiWebSocketMaskKey, + pong_masking_keys: &[WebDriverBiDiWebSocketMaskKey], close_masking_key: WebDriverBiDiWebSocketMaskKey, frame_timeout: Duration, now: &mut dyn FnMut() -> Instant, @@ -94,45 +97,51 @@ impl WebDriverBiDiWebSocketTransportClosureObservation { let connection_generation = established.transport_evidence().connection_generation(); Self::observe_pre_close( established, - pong_masking_key, + pong_masking_keys, close_masking_key, &mut deadline, - true, + MAX_PRE_CLOSE_CONTROL_FRAMES, connection_generation, ) } fn observe_pre_close( established: WebDriverBiDiWebSocketEstablished, - pong_masking_key: WebDriverBiDiWebSocketMaskKey, + pong_masking_keys: &[WebDriverBiDiWebSocketMaskKey], close_masking_key: WebDriverBiDiWebSocketMaskKey, deadline: &mut ClosureDeadline<'_>, - allow_pre_close_control: bool, + remaining_control_frames: usize, connection_generation: WebDriverBiDiConnectionGeneration, ) -> Result { match established.read_frame(deadline.remaining()?) { - Ok((established, frame)) if frame.opcode() == 0xa && allow_pre_close_control => { - Self::observe_pre_close( - established, - pong_masking_key, - close_masking_key, - deadline, - false, - connection_generation, - ) + Ok((_established, frame)) + if matches!(frame.opcode(), 0x9 | 0xa) && remaining_control_frames == 0 => + { + Err(WebDriverBiDiWebSocketTransportClosureError::ControlFrameLimitExceeded) } - Ok((established, frame)) if frame.opcode() == 0x9 && allow_pre_close_control => { + Ok((established, frame)) if frame.opcode() == 0xa => Self::observe_pre_close( + established, + pong_masking_keys, + close_masking_key, + deadline, + remaining_control_frames - 1, + connection_generation, + ), + Ok((established, frame)) if frame.opcode() == 0x9 => { + let (pong_masking_key, remaining_keys) = pong_masking_keys + .split_first() + .ok_or(WebDriverBiDiWebSocketTransportClosureError::PongMaskingKeysExhausted)?; let established = established - .write_pong_frame(frame.payload(), pong_masking_key, deadline.remaining()?) + .write_pong_frame(frame.payload(), *pong_masking_key, deadline.remaining()?) .map_err( |source| WebDriverBiDiWebSocketTransportClosureError::Frame { source }, )?; Self::observe_pre_close( established, - pong_masking_key, + remaining_keys, close_masking_key, deadline, - false, + remaining_control_frames - 1, connection_generation, ) } @@ -218,6 +227,10 @@ impl WebDriverBiDiWebSocketTransportClosureObservation { /// Fail-closed errors while converting one established BiDi transport into closure evidence. #[derive(Debug)] pub enum WebDriverBiDiWebSocketTransportClosureError { + /// More than 64 pre-Close Ping/Pong frames exceeded the local resource budget. + ControlFrameLimitExceeded, + /// A Ping required another caller-supplied masking key; no response was emitted for it. + PongMaskingKeysExhausted, /// The operation-wide deadline expired before transport-closure evidence was admitted. DeadlineExpired, /// The peer sent a valid WebSocket frame outside the bounded closing state machine. @@ -235,6 +248,11 @@ pub enum WebDriverBiDiWebSocketTransportClosureError { impl fmt::Display for WebDriverBiDiWebSocketTransportClosureError { fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { match self { + Self::ControlFrameLimitExceeded => { + formatter.write_str("WebDriver BiDi transport closure control-frame limit exceeded") + } + Self::PongMaskingKeysExhausted => formatter + .write_str("WebDriver BiDi transport closure requires another Pong masking key"), Self::DeadlineExpired => { formatter.write_str("WebDriver BiDi transport closure deadline expired") } @@ -250,7 +268,10 @@ impl fmt::Display for WebDriverBiDiWebSocketTransportClosureError { impl Error for WebDriverBiDiWebSocketTransportClosureError { fn source(&self) -> Option<&(dyn Error + 'static)> { match self { - Self::UnexpectedFrame { .. } | Self::DeadlineExpired => None, + Self::UnexpectedFrame { .. } + | Self::DeadlineExpired + | Self::ControlFrameLimitExceeded + | Self::PongMaskingKeysExhausted => None, Self::Frame { source } => Some(source), } } @@ -314,6 +335,72 @@ mod tests { (established, peer) } + #[test] + fn control_budget_and_key_exhaustion_preserve_exact_failures() { + use WebDriverBiDiWebSocketTransportClosureError::{ + ControlFrameLimitExceeded, PongMaskingKeysExhausted, + }; + for (frames, expected, message) in [ + ( + vec![0x89, 0], + PongMaskingKeysExhausted, + "WebDriver BiDi transport closure requires another Pong masking key", + ), + ( + [vec![0x8a, 0].repeat(64), vec![0x89, 0]].concat(), + ControlFrameLimitExceeded, + "WebDriver BiDi transport closure control-frame limit exceeded", + ), + ( + vec![0x8a, 0].repeat(65), + ControlFrameLimitExceeded, + "WebDriver BiDi transport closure control-frame limit exceeded", + ), + ] { + let (established, mut peer) = established_peer(&frames); + let now = Instant::now(); + let error = WebDriverBiDiWebSocketTransportClosureObservation::observe_with_clock( + established, + &[], + WebDriverBiDiWebSocketMaskKey::new([5, 6, 7, 8]), + Duration::from_secs(1), + &mut || now, + ) + .expect_err("control traffic must stay bounded"); + assert_eq!(format!("{error:?}"), format!("{expected:?}")); + assert_eq!(error.to_string(), message); + assert!(error.source().is_none()); + let mut reply = [0]; + use std::io::Read; + assert_eq!(peer.read(&mut reply).expect("peer EOF"), 0); + } + } + + #[test] + fn exact_control_budget_allows_close_with_fresh_ping_keys() { + for opcode in [0x9, 0xa] { + let frames = [vec![0x80 | opcode, 0].repeat(64), vec![0x88, 0]].concat(); + let (established, _peer) = established_peer(&frames); + let keys: Vec<_> = (0..64) + .map(|index| WebDriverBiDiWebSocketMaskKey::new([index, 1, 2, 3])) + .collect(); + let now = Instant::now(); + let observation = + WebDriverBiDiWebSocketTransportClosureObservation::observe_with_clock( + established, + &keys, + WebDriverBiDiWebSocketMaskKey::new([5, 6, 7, 8]), + Duration::from_secs(1), + &mut || now, + ) + .expect("Close after exactly 64 controls"); + assert_eq!( + observation.kind(), + WebDriverBiDiWebSocketTransportClosureKind::PeerCloseThenEof + ); + } + } + #[test] fn fixed_clock_accepts_complete_closure_before_deadline() { for (frames, kind, status) in [ @@ -348,7 +435,7 @@ mod tests { let observation = WebDriverBiDiWebSocketTransportClosureObservation::observe_with_clock( established, - WebDriverBiDiWebSocketMaskKey::new([1, 2, 3, 4]), + &[WebDriverBiDiWebSocketMaskKey::new([1, 2, 3, 4])], WebDriverBiDiWebSocketMaskKey::new([5, 6, 7, 8]), Duration::from_secs(1), &mut || now, @@ -373,7 +460,7 @@ mod tests { let mut clock_calls = 0; let result = WebDriverBiDiWebSocketTransportClosureObservation::observe_with_clock( established, - WebDriverBiDiWebSocketMaskKey::new([1, 2, 3, 4]), + &[WebDriverBiDiWebSocketMaskKey::new([1, 2, 3, 4])], WebDriverBiDiWebSocketMaskKey::new([5, 6, 7, 8]), timeout, &mut || { @@ -421,7 +508,7 @@ mod tests { }; let error = WebDriverBiDiWebSocketTransportClosureObservation::observe_with_clock( established, - WebDriverBiDiWebSocketMaskKey::new([1, 2, 3, 4]), + &[WebDriverBiDiWebSocketMaskKey::new([1, 2, 3, 4])], WebDriverBiDiWebSocketMaskKey::new([5, 6, 7, 8]), Duration::from_secs(1), &mut now, @@ -509,7 +596,7 @@ mod tests { let now = Instant::now(); let error = WebDriverBiDiWebSocketTransportClosureObservation::observe_with_clock( established, - WebDriverBiDiWebSocketMaskKey::new(pong_key), + &[WebDriverBiDiWebSocketMaskKey::new(pong_key)], WebDriverBiDiWebSocketMaskKey::new(close_key), Duration::from_secs(1), &mut || now, diff --git a/crates/originweave-network/tests/webdriver_bidi_session_teardown_assessment.rs b/crates/originweave-network/tests/webdriver_bidi_session_teardown_assessment.rs index 9c48f8818..545de3d8a 100644 --- a/crates/originweave-network/tests/webdriver_bidi_session_teardown_assessment.rs +++ b/crates/originweave-network/tests/webdriver_bidi_session_teardown_assessment.rs @@ -200,7 +200,7 @@ fn observed_transport_closure( ) -> Result> { let observation = WebDriverBiDiWebSocketTransportClosureObservation::observe( established, - WebDriverBiDiWebSocketMaskKey::new(PONG_MASK_KEY), + &[WebDriverBiDiWebSocketMaskKey::new(PONG_MASK_KEY)], WebDriverBiDiWebSocketMaskKey::new(CLOSE_MASK_KEY), Duration::from_millis(500), )?; diff --git a/crates/originweave-network/tests/webdriver_bidi_transport_close_evidence.rs b/crates/originweave-network/tests/webdriver_bidi_transport_close_evidence.rs index 5f9cab41a..9850efb3b 100644 --- a/crates/originweave-network/tests/webdriver_bidi_transport_close_evidence.rs +++ b/crates/originweave-network/tests/webdriver_bidi_transport_close_evidence.rs @@ -283,7 +283,7 @@ fn observe( > { WebDriverBiDiWebSocketTransportClosureObservation::observe( established, - WebDriverBiDiWebSocketMaskKey::new(PONG_MASK_KEY), + &[WebDriverBiDiWebSocketMaskKey::new(PONG_MASK_KEY)], WebDriverBiDiWebSocketMaskKey::new(CLOSE_MASK_KEY), frame_timeout, ) @@ -415,40 +415,104 @@ fn one_unsolicited_pong_before_close_does_not_block_closure_observation() } #[test] -fn repeated_pong_frames_remain_fail_closed_under_fixed_read_budget() -> Result<(), Box> { +fn repeated_pong_frames_preserve_clean_eof_observation() -> Result<(), Box> { let (established, server) = established_with_server_frame(Some(&[0x8a, 0x00, 0x8a, 0x00]))?; - let Err(error) = observe(established, Duration::from_millis(500)) else { - return Err(io::Error::other( - "repeated Pong frames unexpectedly became transport-closure evidence", - ) - .into()); - }; + let result = observe(established, Duration::from_millis(500)); server .join() .map_err(|_| io::Error::other("repeated-pong test server panicked"))??; - assert!(matches!( - &error, - WebDriverBiDiWebSocketTransportClosureError::UnexpectedFrame { opcode: 0xa } - )); assert_eq!( - error.to_string(), - "WebDriver BiDi peer sent non-closure traffic instead of closing" + result?.kind(), + WebDriverBiDiWebSocketTransportClosureKind::PeerEof ); - assert!(error.source().is_none()); Ok(()) } #[test] -fn ping_after_pre_close_pong_remains_fail_closed() -> Result<(), Box> { - let (established, server) = established_with_server_frame(Some(&[0x8a, 0x00, 0x89, 0x00]))?; +fn ping_after_pre_close_pong_is_answered_before_closure() -> Result<(), Box> { + let (established, server) = established_with_peer_script(|stream| { + stream.write_all(&[0x8a, 0x00, 0x89, 0x01, b'p'])?; + assert_eq!(read_masked_control_frame(stream, 0xa)?, b"p"); + stream.write_all(&[0x88, 0])?; + assert_eq!(read_masked_control_frame(stream, 0x8)?, b""); + Ok(()) + })?; let result = observe(established, Duration::from_millis(500)); - let _ = server.join(); - assert!(matches!( - result, - Err(WebDriverBiDiWebSocketTransportClosureError::UnexpectedFrame { opcode: 0x9 }) - )); + server + .join() + .map_err(|_| io::Error::other("Pong/Ping peer panicked"))??; + assert_eq!( + result?.kind(), + WebDriverBiDiWebSocketTransportClosureKind::PeerCloseThenEof + ); + Ok(()) +} + +#[test] +fn repeated_pings_use_distinct_keys_without_pong_consuming_one() -> Result<(), Box> { + let (established, server) = established_with_peer_script(|stream| { + stream.write_all(&[0x89, 1, b'p'])?; + let mut reply = [0; 7]; + stream.read_exact(&mut reply)?; + assert_eq!(reply, [0x8a, 0x81, 5, 6, 7, 8, 0x75]); + stream.write_all(&[0x8a, 0, 0x89, 1, b'q'])?; + stream.read_exact(&mut reply)?; + assert_eq!(reply, [0x8a, 0x81, 13, 14, 15, 16, 0x7c]); + stream.write_all(&[0x88, 0])?; + assert_eq!(read_masked_control_frame(stream, 0x8)?, b""); + Ok(()) + })?; + let result = WebDriverBiDiWebSocketTransportClosureObservation::observe( + established, + &[ + WebDriverBiDiWebSocketMaskKey::new(PONG_MASK_KEY), + WebDriverBiDiWebSocketMaskKey::new([13, 14, 15, 16]), + ], + WebDriverBiDiWebSocketMaskKey::new(CLOSE_MASK_KEY), + Duration::from_millis(500), + ); + server + .join() + .map_err(|_| io::Error::other("repeated Ping peer panicked"))??; + assert_eq!( + result?.kind(), + WebDriverBiDiWebSocketTransportClosureKind::PeerCloseThenEof + ); + Ok(()) +} + +#[test] +fn repeated_ping_rejects_missing_or_reused_keys_without_second_reply() -> Result<(), Box> +{ + let key = WebDriverBiDiWebSocketMaskKey::new(PONG_MASK_KEY); + for (keys, expected) in [ + (vec![key], "PongMaskingKeysExhausted"), + ( + vec![key, key], + "Frame { source: MalformedFrame { reason: \"client masking key was reused for consecutive frames on this established WebSocket\" } }", + ), + ] { + let (established, server) = established_with_peer_script(|stream| { + stream.write_all(&[0x89, 1, b'p'])?; + assert_eq!(read_masked_control_frame(stream, 0xa)?, b"p"); + stream.write_all(&[0x89, 1, b'q'])?; + let mut reply = [0]; + assert_eq!(stream.read(&mut reply)?, 0); + Ok(()) + })?; + let result = WebDriverBiDiWebSocketTransportClosureObservation::observe( + established, + &keys, + WebDriverBiDiWebSocketMaskKey::new(CLOSE_MASK_KEY), + Duration::from_millis(500), + ); + server + .join() + .map_err(|_| io::Error::other("missing/reused key peer panicked"))??; + assert_eq!(format!("{result:?}"), format!("Err({expected})")); + } Ok(()) } @@ -605,7 +669,7 @@ fn reused_pong_mask_key_preserves_typed_frame_failure() -> Result<(), Box Result<(), Box Result let result = WebDriverBiDiWebSocketTransportClosureObservation::observe( established, - WebDriverBiDiWebSocketMaskKey::new(PONG_MASK_KEY), + &[WebDriverBiDiWebSocketMaskKey::new(PONG_MASK_KEY)], reused, Duration::from_millis(500), ); diff --git a/docs/doctoring.md b/docs/doctoring.md index ee15a050a..b61ee5289 100644 --- a/docs/doctoring.md +++ b/docs/doctoring.md @@ -28,6 +28,17 @@ the monotonic deadline has expired. It prevents per-frame budget renewal without claiming a hard real-time bound on host scheduling or proving process exit/profile cleanup. Tests combine a delayed real peer with deterministic clock transitions. +RFC 6455 sections 5.5.2–5.5.3 permit repeated Ping and unsolicited Pong traffic; +a Ping response preserves its exact payload and an unsolicited Pong needs no reply. +The closure observer now admits up to 64 pre-Close Ping/Pong frames under the same +deadline. This count is a local resource budget, not a protocol limit. The caller +supplies a borrowed masking-key slice consumed once per Ping and a separate Close +key; no entropy provider or callback runs inside the observer. Missing keys fail +before the corresponding response and the existing adjacent-key guard still applies. +The unpublished `observe` API now accepts that slice instead of a single Pong key; +all in-tree consumers are migrated. Boundary tests cover exactly 64 controls, the +65th control, mixed Ping/Pong, exhaustion, and literal independently masked replies. + ### WebSocket Close code admission RFC 6455 section 7.4.2 reserves 1000–2999 for protocol and extension definitions. From 3a8e6f4f89db2f53c144adb3351c153d89adca58 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 8 Sep 2026 12:46:37 +0900 Subject: [PATCH 096/105] test(network): reuse arrays in repeated control fixtures Co-Authored-By: OpenAI Codex --- .../src/webdriver_bidi_websocket_transport_closure.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/originweave-network/src/webdriver_bidi_websocket_transport_closure.rs b/crates/originweave-network/src/webdriver_bidi_websocket_transport_closure.rs index 9d038ac52..f8b03faa8 100644 --- a/crates/originweave-network/src/webdriver_bidi_websocket_transport_closure.rs +++ b/crates/originweave-network/src/webdriver_bidi_websocket_transport_closure.rs @@ -347,12 +347,12 @@ mod tests { "WebDriver BiDi transport closure requires another Pong masking key", ), ( - [vec![0x8a, 0].repeat(64), vec![0x89, 0]].concat(), + [[0x8a, 0].repeat(64), vec![0x89, 0]].concat(), ControlFrameLimitExceeded, "WebDriver BiDi transport closure control-frame limit exceeded", ), ( - vec![0x8a, 0].repeat(65), + [0x8a, 0].repeat(65), ControlFrameLimitExceeded, "WebDriver BiDi transport closure control-frame limit exceeded", ), @@ -379,7 +379,7 @@ mod tests { #[test] fn exact_control_budget_allows_close_with_fresh_ping_keys() { for opcode in [0x9, 0xa] { - let frames = [vec![0x80 | opcode, 0].repeat(64), vec![0x88, 0]].concat(); + let frames = [[0x80 | opcode, 0].repeat(64), vec![0x88, 0]].concat(); let (established, _peer) = established_peer(&frames); let keys: Vec<_> = (0..64) .map(|index| WebDriverBiDiWebSocketMaskKey::new([index, 1, 2, 3])) From 10d5e1ff78d46bbad004d4e0971d0ffceac757fa Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 8 Sep 2026 13:04:01 +0900 Subject: [PATCH 097/105] test(network): reject server Close 1010 --- ...er_bidi_transport_close_role_validation.rs | 160 ++++++++++++++++++ 1 file changed, 160 insertions(+) create mode 100644 crates/originweave-network/tests/webdriver_bidi_transport_close_role_validation.rs diff --git a/crates/originweave-network/tests/webdriver_bidi_transport_close_role_validation.rs b/crates/originweave-network/tests/webdriver_bidi_transport_close_role_validation.rs new file mode 100644 index 000000000..ea3b80daf --- /dev/null +++ b/crates/originweave-network/tests/webdriver_bidi_transport_close_role_validation.rs @@ -0,0 +1,160 @@ +use std::{ + error::Error, + io::{self, Read, Write}, + net::{TcpListener, TcpStream}, + thread, + time::Duration, +}; + +use originweave_core::WebDriverBiDiWebSocketEndpoint; +use originweave_network::{ + WebDriverBiDiTcpConnectionPlan, WebDriverBiDiWebSocketClientKey, + WebDriverBiDiWebSocketFrameError, WebDriverBiDiWebSocketHandshakePlan, + WebDriverBiDiWebSocketMaskKey, WebDriverBiDiWebSocketTransportClosureError, + WebDriverBiDiWebSocketTransportClosureKind, WebDriverBiDiWebSocketTransportClosureObservation, +}; + +const SESSION_ID: &str = "01234567-89ab-cdef-0123-456789abcdef"; +const RFC6455_SAMPLE_KEY: &str = "dGhlIHNhbXBsZSBub25jZQ=="; +const OPENING_RESPONSE: &[u8] = b"HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=\r\n\r\n"; +const CLOSE_MASK_KEY: [u8; 4] = [9, 10, 11, 12]; + +fn read_opening_request(stream: &mut TcpStream) -> io::Result<()> { + stream.set_read_timeout(Some(Duration::from_secs(2)))?; + let mut request = Vec::new(); + let mut buffer = [0_u8; 512]; + while !request.ends_with(b"\r\n\r\n") { + let count = stream.read(&mut buffer)?; + if count == 0 { + return Err(io::Error::new( + io::ErrorKind::UnexpectedEof, + "client opening request ended before the header terminator", + )); + } + request.extend_from_slice(&buffer[..count]); + } + Ok(()) +} + +fn read_optional_masked_close(stream: &mut TcpStream) -> io::Result>> { + stream.set_read_timeout(Some(Duration::from_millis(200)))?; + let mut header = [0_u8; 2]; + match stream.read(&mut header[..1]) { + Ok(0) => return Ok(None), + Ok(1) => {} + Ok(_) => unreachable!("one-byte header read returned more than one byte"), + Err(source) + if matches!( + source.kind(), + io::ErrorKind::TimedOut | io::ErrorKind::WouldBlock + ) => + { + return Ok(None); + } + Err(source) => return Err(source), + } + stream.read_exact(&mut header[1..])?; + if header[0] != 0x88 || header[1] & 0x80 == 0 { + return Err(io::Error::new( + io::ErrorKind::InvalidData, + "expected one final masked client Close frame", + )); + } + let payload_len = usize::from(header[1] & 0x7f); + if payload_len > 125 { + return Err(io::Error::new( + io::ErrorKind::InvalidData, + "client Close payload exceeded RFC 6455 control-frame bound", + )); + } + let mut mask = [0_u8; 4]; + stream.read_exact(&mut mask)?; + let mut payload = vec![0_u8; payload_len]; + stream.read_exact(&mut payload)?; + for (index, byte) in payload.iter_mut().enumerate() { + *byte ^= mask[index % mask.len()]; + } + Ok(Some(payload)) +} + +fn established_with_server_close( + status_code: u16, +) -> Result< + ( + originweave_network::WebDriverBiDiWebSocketEstablished, + thread::JoinHandle>>>, + ), + Box, +> { + let listener = TcpListener::bind(("127.0.0.1", 0))?; + let local_addr = listener.local_addr()?; + let server = thread::spawn(move || -> io::Result>> { + let (mut stream, _) = listener.accept()?; + read_opening_request(&mut stream)?; + stream.write_all(OPENING_RESPONSE)?; + let [high, low] = status_code.to_be_bytes(); + stream.write_all(&[0x88, 0x02, high, low])?; + read_optional_masked_close(&mut stream) + }); + + let endpoint = format!("ws://{local_addr}/session/{SESSION_ID}"); + let target = WebDriverBiDiWebSocketEndpoint::new(&endpoint)? + .correlate_session_id(SESSION_ID)? + .into_explicit_connect_target()?; + let connection = + WebDriverBiDiTcpConnectionPlan::new(target, Duration::from_secs(1), 1)?.connect()?; + let key = WebDriverBiDiWebSocketClientKey::new(RFC6455_SAMPLE_KEY)?; + let established = WebDriverBiDiWebSocketHandshakePlan::new(connection, key)? + .write_opening_request(Duration::from_millis(500))? + .read_opening_response(Duration::from_millis(500))?; + Ok((established, server)) +} + +fn observe( + established: originweave_network::WebDriverBiDiWebSocketEstablished, +) -> Result< + WebDriverBiDiWebSocketTransportClosureObservation, + WebDriverBiDiWebSocketTransportClosureError, +> { + WebDriverBiDiWebSocketTransportClosureObservation::observe( + established, + &[], + WebDriverBiDiWebSocketMaskKey::new(CLOSE_MASK_KEY), + Duration::from_millis(250), + ) +} + +#[test] +fn server_close_1010_is_rejected_before_reply_or_closure_evidence() -> Result<(), Box> { + let (established, server) = established_with_server_close(1010)?; + let result = observe(established); + let reply = server + .join() + .map_err(|_| io::Error::other("role-invalid Close peer panicked"))??; + + assert!(reply.is_none(), "server Close(1010) was mirrored by the client"); + assert!(matches!( + result, + Err(WebDriverBiDiWebSocketTransportClosureError::Frame { + source: WebDriverBiDiWebSocketFrameError::MalformedFrame { .. } + }) + )); + Ok(()) +} + +#[test] +fn server_close_1011_remains_valid_and_is_mirrored() -> Result<(), Box> { + let (established, server) = established_with_server_close(1011)?; + let observation = observe(established)?; + let reply = server + .join() + .map_err(|_| io::Error::other("valid server Close peer panicked"))??; + + assert_eq!(reply, Some(1011_u16.to_be_bytes().to_vec())); + assert_eq!( + observation.kind(), + WebDriverBiDiWebSocketTransportClosureKind::PeerCloseThenEof + ); + assert_eq!(observation.peer_close_status_code(), Some(1011)); + Ok(()) +} From 6b6c90ed3919ea84b69527ab688a087eeb45224d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 8 Sep 2026 13:30:40 +0900 Subject: [PATCH 098/105] fix(network): reject server Close 1010 Co-authored-by: OpenAI Codex --- AGENTS.md | 5 ++++ CHANGELOG.md | 2 ++ ...driver_bidi_websocket_transport_closure.rs | 29 ++++++++++++++++++- ...er_bidi_transport_close_role_validation.rs | 19 ++++++------ docs/doctoring.md | 7 +++-- 5 files changed, 49 insertions(+), 13 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index b77c64961..ad94ed713 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -115,6 +115,11 @@ A skipped security, GPU, browser, TLS, or statistical test is not passing eviden ### Coverage diagnosis lesson +Keep generic wire-code validation separate from endpoint-role validation. For +WebSocket Close, reject server-sent client-only 1010 in the client closure state +machine before echoing it, and pair that RED case with valid server 1011 so a +blanket rejection cannot satisfy the regression. + Repeated control-frame support must preserve the total deadline and per-write masking authority. Supply a separate caller-owned key per Ping, consume none for unsolicited Pong, and keep the Close key independent. Test exact-budget success, budget overflow, exhausted keys, and reused keys with a peer that verifies no rejected reply. The 64-control budget is local resource policy, not an RFC limit. For Close-code validation, check the current IANA registry as well as RFC 6455: the protocol-reserved range is not an allowance for unassigned values. Keep application/private ranges separate, test assigned and reserved boundaries, and verify rejected peer codes produce neither an echo nor closure evidence. Record the registry date; future assignments need a reviewed update, not ambient network lookup during frame parsing. diff --git a/CHANGELOG.md b/CHANGELOG.md index 901630df8..ccf2f4eb1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,8 @@ All notable changes to OriginWeave are documented in this file. The format follo ### Changed +- Browser transport shutdown rejects the client-only Close status 1010 when received from a server, before sending a reply or recording completion; server status 1011 remains supported. + - Browser transport shutdown now handles repeated keepalive traffic within one time budget, using a separate supplied key for each reply and rejecting exhausted keys or excess traffic without reporting completion. - Browser transport shutdown rejects unassigned protocol close codes before replying or recording completion; application and private-use code ranges remain supported. diff --git a/crates/originweave-network/src/webdriver_bidi_websocket_transport_closure.rs b/crates/originweave-network/src/webdriver_bidi_websocket_transport_closure.rs index f8b03faa8..abd5a12bb 100644 --- a/crates/originweave-network/src/webdriver_bidi_websocket_transport_closure.rs +++ b/crates/originweave-network/src/webdriver_bidi_websocket_transport_closure.rs @@ -63,7 +63,7 @@ impl WebDriverBiDiWebSocketTransportClosureObservation { /// the required Close response. Up to 64 pre-Close Ping/Pong frames are admitted as a local /// resource limit. Pings consume keys in slice order; unsolicited Pongs consume no key. /// Exhausted keys, excess control traffic, application data, timeout, partial EOF, malformed frames, write - /// failures, or any post-Close frame fail closed. Peer Close alone is never success: after the + /// failures, a server-sent client-only status 1010, or any post-Close frame fail closed. Peer Close alone is never success: after the /// masked response this boundary requires zero-byte TCP EOF within one operation-wide /// deadline covering all reads and writes. Evidence arriving after that deadline is rejected. pub fn observe( @@ -150,6 +150,13 @@ impl WebDriverBiDiWebSocketTransportClosureObservation { .payload() .get(..2) .map(|bytes| u16::from_be_bytes([bytes[0], bytes[1]])); + if peer_close_status_code == Some(1010) { + return Err(WebDriverBiDiWebSocketTransportClosureError::Frame { + source: WebDriverBiDiWebSocketFrameError::MalformedFrame { + reason: "Close status 1010 is reserved for clients", + }, + }); + } let established = established .write_close_frame( peer_close_status_code, @@ -446,6 +453,26 @@ mod tests { } } + #[test] + fn client_only_close_status_is_rejected_before_reply() { + let (established, mut peer) = established_peer(&[0x88, 2, 3, 0xf2]); + let now = Instant::now(); + let error = WebDriverBiDiWebSocketTransportClosureObservation::observe_with_clock( + established, + &[], + WebDriverBiDiWebSocketMaskKey::new([5, 6, 7, 8]), + Duration::from_secs(1), + &mut || now, + ) + .expect_err("server Close 1010 must fail"); + assert_eq!( + format!("{error:?}"), + "Frame { source: MalformedFrame { reason: \"Close status 1010 is reserved for clients\" } }" + ); + let mut reply = [0]; + assert_eq!(peer.read(&mut reply).expect("peer EOF"), 0); + } + #[test] fn invalid_deadline_is_rejected_before_clock_use() { for (timeout, expected_calls) in [ diff --git a/crates/originweave-network/tests/webdriver_bidi_transport_close_role_validation.rs b/crates/originweave-network/tests/webdriver_bidi_transport_close_role_validation.rs index ea3b80daf..af4aa7def 100644 --- a/crates/originweave-network/tests/webdriver_bidi_transport_close_role_validation.rs +++ b/crates/originweave-network/tests/webdriver_bidi_transport_close_role_validation.rs @@ -18,6 +18,10 @@ const SESSION_ID: &str = "01234567-89ab-cdef-0123-456789abcdef"; const RFC6455_SAMPLE_KEY: &str = "dGhlIHNhbXBsZSBub25jZQ=="; const OPENING_RESPONSE: &[u8] = b"HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=\r\n\r\n"; const CLOSE_MASK_KEY: [u8; 4] = [9, 10, 11, 12]; +type EstablishedPeer = ( + originweave_network::WebDriverBiDiWebSocketEstablished, + thread::JoinHandle>>>, +); fn read_opening_request(stream: &mut TcpStream) -> io::Result<()> { stream.set_read_timeout(Some(Duration::from_secs(2)))?; @@ -77,15 +81,7 @@ fn read_optional_masked_close(stream: &mut TcpStream) -> io::Result Result< - ( - originweave_network::WebDriverBiDiWebSocketEstablished, - thread::JoinHandle>>>, - ), - Box, -> { +fn established_with_server_close(status_code: u16) -> Result> { let listener = TcpListener::bind(("127.0.0.1", 0))?; let local_addr = listener.local_addr()?; let server = thread::spawn(move || -> io::Result>> { @@ -132,7 +128,10 @@ fn server_close_1010_is_rejected_before_reply_or_closure_evidence() -> Result<() .join() .map_err(|_| io::Error::other("role-invalid Close peer panicked"))??; - assert!(reply.is_none(), "server Close(1010) was mirrored by the client"); + assert!( + reply.is_none(), + "server Close(1010) was mirrored by the client" + ); assert!(matches!( result, Err(WebDriverBiDiWebSocketTransportClosureError::Frame { diff --git a/docs/doctoring.md b/docs/doctoring.md index b61ee5289..9d98997c7 100644 --- a/docs/doctoring.md +++ b/docs/doctoring.md @@ -49,12 +49,15 @@ ordinary wire status codes. It retains 3000–3999 for application codes and 4000–4999 for private use without interpreting their meanings or treating them as proof of successful browser work. This is a reviewed static admission policy, not an online registry check. New protocol assignments require a reviewed update. -The shared frame validator runs before any Close echo or closure evidence. +The shared frame validator runs before role-specific handling. RFC 6455 section +7.4.1 assigns status 1010 to clients and says servers do not use it, so the +client-side transport-closure state machine rejects a peer 1010 before any +Close echo or closure evidence. Server status 1011 remains admissible. Internet Assigned Numbers Authority. (2026). *WebSocket protocol registries*. Retrieved September 8, 2026, from https://www.iana.org/assignments/websocket -Fette, I., & Melnikov, A. (2011). *The WebSocket protocol* (RFC 6455, § 7.4.2). +Fette, I., & Melnikov, A. (2011). *The WebSocket protocol* (RFC 6455, §§ 7.4.1–7.4.2). Internet Engineering Task Force. https://www.rfc-editor.org/rfc/rfc6455 ### Browser origin equivalence From ce4ef7188a289bca836cb8f00cd76737b4eb4196 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 8 Sep 2026 15:09:29 +0900 Subject: [PATCH 099/105] test(network): require role-specific server Close error --- .../webdriver_bidi_transport_close_role_validation.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/crates/originweave-network/tests/webdriver_bidi_transport_close_role_validation.rs b/crates/originweave-network/tests/webdriver_bidi_transport_close_role_validation.rs index af4aa7def..77439d3a6 100644 --- a/crates/originweave-network/tests/webdriver_bidi_transport_close_role_validation.rs +++ b/crates/originweave-network/tests/webdriver_bidi_transport_close_role_validation.rs @@ -9,9 +9,9 @@ use std::{ use originweave_core::WebDriverBiDiWebSocketEndpoint; use originweave_network::{ WebDriverBiDiTcpConnectionPlan, WebDriverBiDiWebSocketClientKey, - WebDriverBiDiWebSocketFrameError, WebDriverBiDiWebSocketHandshakePlan, - WebDriverBiDiWebSocketMaskKey, WebDriverBiDiWebSocketTransportClosureError, - WebDriverBiDiWebSocketTransportClosureKind, WebDriverBiDiWebSocketTransportClosureObservation, + WebDriverBiDiWebSocketHandshakePlan, WebDriverBiDiWebSocketMaskKey, + WebDriverBiDiWebSocketTransportClosureError, WebDriverBiDiWebSocketTransportClosureKind, + WebDriverBiDiWebSocketTransportClosureObservation, }; const SESSION_ID: &str = "01234567-89ab-cdef-0123-456789abcdef"; @@ -134,8 +134,8 @@ fn server_close_1010_is_rejected_before_reply_or_closure_evidence() -> Result<() ); assert!(matches!( result, - Err(WebDriverBiDiWebSocketTransportClosureError::Frame { - source: WebDriverBiDiWebSocketFrameError::MalformedFrame { .. } + Err(WebDriverBiDiWebSocketTransportClosureError::PeerCloseStatusNotAllowed { + status_code: 1010 }) )); Ok(()) From 39f054f6b3c319a525331d4b25966ff4f80b123e Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 8 Sep 2026 15:16:15 +0900 Subject: [PATCH 100/105] fix(network): separate peer Close role error --- ...driver_bidi_websocket_transport_closure.rs | 38 +++++++++++++------ 1 file changed, 27 insertions(+), 11 deletions(-) diff --git a/crates/originweave-network/src/webdriver_bidi_websocket_transport_closure.rs b/crates/originweave-network/src/webdriver_bidi_websocket_transport_closure.rs index abd5a12bb..73cda8f55 100644 --- a/crates/originweave-network/src/webdriver_bidi_websocket_transport_closure.rs +++ b/crates/originweave-network/src/webdriver_bidi_websocket_transport_closure.rs @@ -62,10 +62,11 @@ impl WebDriverBiDiWebSocketTransportClosureObservation { /// The caller supplies independent fresh masking keys for each pre-Close Ping response and /// the required Close response. Up to 64 pre-Close Ping/Pong frames are admitted as a local /// resource limit. Pings consume keys in slice order; unsolicited Pongs consume no key. - /// Exhausted keys, excess control traffic, application data, timeout, partial EOF, malformed frames, write - /// failures, a server-sent client-only status 1010, or any post-Close frame fail closed. Peer Close alone is never success: after the - /// masked response this boundary requires zero-byte TCP EOF within one operation-wide - /// deadline covering all reads and writes. Evidence arriving after that deadline is rejected. + /// Exhausted keys, excess control traffic, application data, timeout, partial EOF, malformed + /// frames, write failures, a server-sent client-only status 1010, or any post-Close frame fail + /// closed. Peer Close alone is never success: after the masked response this boundary requires + /// zero-byte TCP EOF within one operation-wide deadline covering all reads and writes. Evidence + /// arriving after that deadline is rejected. pub fn observe( established: WebDriverBiDiWebSocketEstablished, pong_masking_keys: &[WebDriverBiDiWebSocketMaskKey], @@ -151,11 +152,11 @@ impl WebDriverBiDiWebSocketTransportClosureObservation { .get(..2) .map(|bytes| u16::from_be_bytes([bytes[0], bytes[1]])); if peer_close_status_code == Some(1010) { - return Err(WebDriverBiDiWebSocketTransportClosureError::Frame { - source: WebDriverBiDiWebSocketFrameError::MalformedFrame { - reason: "Close status 1010 is reserved for clients", + return Err( + WebDriverBiDiWebSocketTransportClosureError::PeerCloseStatusNotAllowed { + status_code: 1010, }, - }); + ); } let established = established .write_close_frame( @@ -240,6 +241,11 @@ pub enum WebDriverBiDiWebSocketTransportClosureError { PongMaskingKeysExhausted, /// The operation-wide deadline expired before transport-closure evidence was admitted. DeadlineExpired, + /// The server sent a wire-valid Close status whose meaning is reserved for clients. + PeerCloseStatusNotAllowed { + /// Exact peer-supplied Close status rejected by the known client/server role boundary. + status_code: u16, + }, /// The peer sent a valid WebSocket frame outside the bounded closing state machine. UnexpectedFrame { /// Exact validated RFC 6455 opcode observed instead of admissible bounded closing traffic. @@ -263,6 +269,8 @@ impl fmt::Display for WebDriverBiDiWebSocketTransportClosureError { Self::DeadlineExpired => { formatter.write_str("WebDriver BiDi transport closure deadline expired") } + Self::PeerCloseStatusNotAllowed { .. } => formatter + .write_str("WebDriver BiDi server sent a Close status reserved for clients"), Self::UnexpectedFrame { .. } => formatter .write_str("WebDriver BiDi peer sent non-closure traffic instead of closing"), Self::Frame { .. } => { @@ -278,7 +286,8 @@ impl Error for WebDriverBiDiWebSocketTransportClosureError { Self::UnexpectedFrame { .. } | Self::DeadlineExpired | Self::ControlFrameLimitExceeded - | Self::PongMaskingKeysExhausted => None, + | Self::PongMaskingKeysExhausted + | Self::PeerCloseStatusNotAllowed { .. } => None, Self::Frame { source } => Some(source), } } @@ -465,10 +474,17 @@ mod tests { &mut || now, ) .expect_err("server Close 1010 must fail"); + assert!(matches!( + error, + WebDriverBiDiWebSocketTransportClosureError::PeerCloseStatusNotAllowed { + status_code: 1010 + } + )); assert_eq!( - format!("{error:?}"), - "Frame { source: MalformedFrame { reason: \"Close status 1010 is reserved for clients\" } }" + error.to_string(), + "WebDriver BiDi server sent a Close status reserved for clients" ); + assert!(error.source().is_none()); let mut reply = [0]; assert_eq!(peer.read(&mut reply).expect("peer EOF"), 0); } From cc3bf6dc933425af95716fcfb287b7fabedb19fb Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 8 Sep 2026 15:16:48 +0900 Subject: [PATCH 101/105] test(network): assert typed peer Close role error --- ...ebdriver_bidi_transport_close_role_validation.rs | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/crates/originweave-network/tests/webdriver_bidi_transport_close_role_validation.rs b/crates/originweave-network/tests/webdriver_bidi_transport_close_role_validation.rs index 77439d3a6..653823959 100644 --- a/crates/originweave-network/tests/webdriver_bidi_transport_close_role_validation.rs +++ b/crates/originweave-network/tests/webdriver_bidi_transport_close_role_validation.rs @@ -123,7 +123,7 @@ fn observe( #[test] fn server_close_1010_is_rejected_before_reply_or_closure_evidence() -> Result<(), Box> { let (established, server) = established_with_server_close(1010)?; - let result = observe(established); + let error = observe(established).expect_err("server Close 1010 must be role-rejected"); let reply = server .join() .map_err(|_| io::Error::other("role-invalid Close peer panicked"))??; @@ -133,11 +133,16 @@ fn server_close_1010_is_rejected_before_reply_or_closure_evidence() -> Result<() "server Close(1010) was mirrored by the client" ); assert!(matches!( - result, - Err(WebDriverBiDiWebSocketTransportClosureError::PeerCloseStatusNotAllowed { + &error, + WebDriverBiDiWebSocketTransportClosureError::PeerCloseStatusNotAllowed { status_code: 1010 - }) + } )); + assert_eq!( + error.to_string(), + "WebDriver BiDi server sent a Close status reserved for clients" + ); + assert!(error.source().is_none()); Ok(()) } From 30b45879e8f3cc3b24244a85524f36ee8898c784 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 8 Sep 2026 15:18:24 +0900 Subject: [PATCH 102/105] fix(test): retain peer Close error for assertions --- .../src/webdriver_bidi_websocket_transport_closure.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/originweave-network/src/webdriver_bidi_websocket_transport_closure.rs b/crates/originweave-network/src/webdriver_bidi_websocket_transport_closure.rs index 73cda8f55..5efb37b4b 100644 --- a/crates/originweave-network/src/webdriver_bidi_websocket_transport_closure.rs +++ b/crates/originweave-network/src/webdriver_bidi_websocket_transport_closure.rs @@ -475,7 +475,7 @@ mod tests { ) .expect_err("server Close 1010 must fail"); assert!(matches!( - error, + &error, WebDriverBiDiWebSocketTransportClosureError::PeerCloseStatusNotAllowed { status_code: 1010 } From 9e51537946f85874e340b484843a80c82ef42b3d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 8 Sep 2026 15:37:35 +0900 Subject: [PATCH 103/105] fix(test): satisfy strict clippy in Close role regression --- .../webdriver_bidi_transport_close_role_validation.rs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/crates/originweave-network/tests/webdriver_bidi_transport_close_role_validation.rs b/crates/originweave-network/tests/webdriver_bidi_transport_close_role_validation.rs index 653823959..6f6117ffb 100644 --- a/crates/originweave-network/tests/webdriver_bidi_transport_close_role_validation.rs +++ b/crates/originweave-network/tests/webdriver_bidi_transport_close_role_validation.rs @@ -123,7 +123,12 @@ fn observe( #[test] fn server_close_1010_is_rejected_before_reply_or_closure_evidence() -> Result<(), Box> { let (established, server) = established_with_server_close(1010)?; - let error = observe(established).expect_err("server Close 1010 must be role-rejected"); + let error = match observe(established) { + Ok(_) => { + return Err(io::Error::other("server Close 1010 produced closure evidence").into()); + } + Err(error) => error, + }; let reply = server .join() .map_err(|_| io::Error::other("role-invalid Close peer panicked"))??; From a4371639d4977d98862339656996489780f00daa Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 8 Sep 2026 16:11:03 +0900 Subject: [PATCH 104/105] test(network): remove duplicate Close role fixture --- ...driver_bidi_websocket_transport_closure.rs | 27 ------------------- 1 file changed, 27 deletions(-) diff --git a/crates/originweave-network/src/webdriver_bidi_websocket_transport_closure.rs b/crates/originweave-network/src/webdriver_bidi_websocket_transport_closure.rs index 5efb37b4b..0269f3241 100644 --- a/crates/originweave-network/src/webdriver_bidi_websocket_transport_closure.rs +++ b/crates/originweave-network/src/webdriver_bidi_websocket_transport_closure.rs @@ -462,33 +462,6 @@ mod tests { } } - #[test] - fn client_only_close_status_is_rejected_before_reply() { - let (established, mut peer) = established_peer(&[0x88, 2, 3, 0xf2]); - let now = Instant::now(); - let error = WebDriverBiDiWebSocketTransportClosureObservation::observe_with_clock( - established, - &[], - WebDriverBiDiWebSocketMaskKey::new([5, 6, 7, 8]), - Duration::from_secs(1), - &mut || now, - ) - .expect_err("server Close 1010 must fail"); - assert!(matches!( - &error, - WebDriverBiDiWebSocketTransportClosureError::PeerCloseStatusNotAllowed { - status_code: 1010 - } - )); - assert_eq!( - error.to_string(), - "WebDriver BiDi server sent a Close status reserved for clients" - ); - assert!(error.source().is_none()); - let mut reply = [0]; - assert_eq!(peer.read(&mut reply).expect("peer EOF"), 0); - } - #[test] fn invalid_deadline_is_rejected_before_clock_use() { for (timeout, expected_calls) in [ From ca490312647516d94a8edc5854be1d5f77eb5b6d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 8 Sep 2026 17:09:05 +0900 Subject: [PATCH 105/105] test(network): share close-role regression with unit coverage --- crates/originweave-network/src/lib.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/crates/originweave-network/src/lib.rs b/crates/originweave-network/src/lib.rs index 6012bd124..9ebfa3588 100644 --- a/crates/originweave-network/src/lib.rs +++ b/crates/originweave-network/src/lib.rs @@ -38,6 +38,14 @@ mod webdriver_bidi_websocket_transport_closure; #[cfg(test)] mod webdriver_bidi_json_envelope_public_boundary_tests; +// LLVM coverage keeps the crate unit-test instantiation separate from integration-test binaries, +// so compile the same realistic 1010/1011 loopback contract here instead of maintaining a copy. +#[cfg(test)] +extern crate self as originweave_network; +#[cfg(test)] +#[path = "../tests/webdriver_bidi_transport_close_role_validation.rs"] +mod webdriver_bidi_transport_close_role_validation_unit; + pub use connection::{ ConnectionPlan, DirectTcpConnection, MAX_CONNECT_TIMEOUT, MAX_CONNECTION_ATTEMPTS, NetworkError, SocketConnectionEvidence,