test: harden malformed client handling
This commit is contained in:
1 parent
07de17a399
commit
d18d5b5fc4
9 files changed
+380
-15
No files matched your search
Generated
+4
@@ -458,6 +458,10 @@ dependencies = [
|
|||||||
name = "ferese-layout"
|
name = "ferese-layout"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ferese-malformed-client"
|
||||||
|
version = "0.1.0"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "feresectl"
|
name = "feresectl"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ members = [
|
|||||||
"crates/ferese-core",
|
"crates/ferese-core",
|
||||||
"crates/ferese-ipc",
|
"crates/ferese-ipc",
|
||||||
"crates/ferese-layout",
|
"crates/ferese-layout",
|
||||||
|
"tools/ferese-malformed-client",
|
||||||
]
|
]
|
||||||
resolver = "3"
|
resolver = "3"
|
||||||
|
|
||||||
|
|||||||
@@ -201,7 +201,8 @@ The v0 IPC slice also supports `move`, `resize`, `move-to-workspace`,
|
|||||||
`center-column`, `consume`, `expel`, `close`, `get-focused-window`, and
|
`center-column`, `consume`, `expel`, `close`, `get-focused-window`, and
|
||||||
`get-outputs`. Requests use versioned, 1 MiB-limited length-prefixed JSON. The
|
`get-outputs`. Requests use versioned, 1 MiB-limited length-prefixed JSON. The
|
||||||
runtime directory and socket are restricted to modes `0700` and `0600`, and the
|
runtime directory and socket are restricted to modes `0700` and `0600`, and the
|
||||||
server rejects peers whose effective UID differs from the compositor's.
|
server rejects peers whose effective UID differs from the compositor's. IPC has
|
||||||
|
a bounded request queue and accepts at most 64 concurrent client connections.
|
||||||
`get-outputs` includes disabled-but-connected monitors, connector and persistent
|
`get-outputs` includes disabled-but-connected monitors, connector and persistent
|
||||||
identity matchers, current and available modes with refresh rates, fractional
|
identity matchers, current and available modes with refresh rates, fractional
|
||||||
scale, transform, logical position and size, and physical dimensions.
|
scale, transform, logical position and size, and physical dimensions.
|
||||||
|
|||||||
+187
-11
@@ -5,7 +5,11 @@ use std::os::fd::AsRawFd;
|
|||||||
use std::os::unix::fs::{FileTypeExt, MetadataExt, PermissionsExt};
|
use std::os::unix::fs::{FileTypeExt, MetadataExt, PermissionsExt};
|
||||||
use std::os::unix::net::{UnixListener, UnixStream};
|
use std::os::unix::net::{UnixListener, UnixStream};
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use std::sync::mpsc::{SyncSender, sync_channel};
|
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||||
|
use std::sync::{
|
||||||
|
Arc,
|
||||||
|
mpsc::{SyncSender, sync_channel},
|
||||||
|
};
|
||||||
use std::thread;
|
use std::thread;
|
||||||
|
|
||||||
use ferese_core::LayoutMode;
|
use ferese_core::LayoutMode;
|
||||||
@@ -18,6 +22,7 @@ use smithay::utils::Transform;
|
|||||||
use crate::{Ferese, config::OutputTransform};
|
use crate::{Ferese, config::OutputTransform};
|
||||||
|
|
||||||
const REQUEST_QUEUE_CAPACITY: usize = 128;
|
const REQUEST_QUEUE_CAPACITY: usize = 128;
|
||||||
|
const MAX_CONNECTIONS: usize = 64;
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
struct IpcCall {
|
struct IpcCall {
|
||||||
@@ -25,6 +30,16 @@ struct IpcCall {
|
|||||||
response: SyncSender<Response>,
|
response: SyncSender<Response>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct ConnectionPermit {
|
||||||
|
active: Arc<AtomicUsize>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for ConnectionPermit {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
self.active.fetch_sub(1, Ordering::AcqRel);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub(crate) struct IpcSocketGuard {
|
pub(crate) struct IpcSocketGuard {
|
||||||
path: PathBuf,
|
path: PathBuf,
|
||||||
@@ -78,6 +93,8 @@ pub(crate) fn init(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn accept_connections(listener: UnixListener, sender: channel::SyncSender<IpcCall>) {
|
fn accept_connections(listener: UnixListener, sender: channel::SyncSender<IpcCall>) {
|
||||||
|
let active_connections = Arc::new(AtomicUsize::new(0));
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
let (stream, _) = match listener.accept() {
|
let (stream, _) = match listener.accept() {
|
||||||
Ok(connection) => connection,
|
Ok(connection) => connection,
|
||||||
@@ -100,16 +117,43 @@ fn accept_connections(listener: UnixListener, sender: channel::SyncSender<IpcCal
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let Some(permit) = try_acquire_connection(&active_connections) else {
|
||||||
|
tracing::warn!(
|
||||||
|
limit = MAX_CONNECTIONS,
|
||||||
|
"rejected IPC connection at worker limit"
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
};
|
||||||
let sender = sender.clone();
|
let sender = sender.clone();
|
||||||
if let Err(error) = thread::Builder::new()
|
if let Err(error) = thread::Builder::new()
|
||||||
.name("ferese-ipc-client".to_owned())
|
.name("ferese-ipc-client".to_owned())
|
||||||
.spawn(move || serve_connection(stream, sender))
|
.spawn(move || {
|
||||||
|
let _permit = permit;
|
||||||
|
serve_connection(stream, sender);
|
||||||
|
})
|
||||||
{
|
{
|
||||||
tracing::warn!(%error, "could not start IPC connection worker");
|
tracing::warn!(%error, "could not start IPC connection worker");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn try_acquire_connection(active: &Arc<AtomicUsize>) -> Option<ConnectionPermit> {
|
||||||
|
let mut count = active.load(Ordering::Acquire);
|
||||||
|
loop {
|
||||||
|
if count >= MAX_CONNECTIONS {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
match active.compare_exchange_weak(count, count + 1, Ordering::AcqRel, Ordering::Acquire) {
|
||||||
|
Ok(_) => break,
|
||||||
|
Err(actual) => count = actual,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(ConnectionPermit {
|
||||||
|
active: active.clone(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
fn serve_connection(mut stream: UnixStream, sender: channel::SyncSender<IpcCall>) {
|
fn serve_connection(mut stream: UnixStream, sender: channel::SyncSender<IpcCall>) {
|
||||||
loop {
|
loop {
|
||||||
let request = match read_frame(&mut stream) {
|
let request = match read_frame(&mut stream) {
|
||||||
@@ -147,15 +191,8 @@ fn serve_connection(mut stream: UnixStream, sender: channel::SyncSender<IpcCall>
|
|||||||
|
|
||||||
impl Ferese {
|
impl Ferese {
|
||||||
fn handle_ipc_request(&mut self, request: Request) -> Response {
|
fn handle_ipc_request(&mut self, request: Request) -> Response {
|
||||||
if request.version != VERSION {
|
if let Err(error) = validate_request(&request) {
|
||||||
return Response::error(
|
return Response::error(request.id, error.code, error.message);
|
||||||
request.id,
|
|
||||||
"unsupported_version",
|
|
||||||
format!("supported version is {VERSION}"),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if request.kind != "command" {
|
|
||||||
return Response::error(request.id, "invalid_type", "expected type \"command\"");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
match self.dispatch_ipc_command(&request.command, &request.args) {
|
match self.dispatch_ipc_command(&request.command, &request.args) {
|
||||||
@@ -345,6 +382,23 @@ impl Ferese {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn validate_request(request: &Request) -> Result<(), CommandError> {
|
||||||
|
if request.version != VERSION {
|
||||||
|
return Err(CommandError::new(
|
||||||
|
"unsupported_version",
|
||||||
|
format!("supported version is {VERSION}"),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if request.kind != "command" {
|
||||||
|
return Err(CommandError::new(
|
||||||
|
"invalid_type",
|
||||||
|
"expected type \"command\"",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
fn transform_name(transform: Transform) -> &'static str {
|
fn transform_name(transform: Transform) -> &'static str {
|
||||||
match transform {
|
match transform {
|
||||||
Transform::Normal => "normal",
|
Transform::Normal => "normal",
|
||||||
@@ -500,8 +554,22 @@ fn effective_uid() -> u32 {
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
|
use std::io::{Read, Write};
|
||||||
|
use std::net::Shutdown;
|
||||||
|
use std::os::unix::fs::PermissionsExt;
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
fn request(version: u32, kind: &str) -> Request {
|
||||||
|
Request {
|
||||||
|
version,
|
||||||
|
id: 7,
|
||||||
|
kind: kind.to_owned(),
|
||||||
|
command: "get-outputs".to_owned(),
|
||||||
|
args: json!({}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn parses_direction_arguments() {
|
fn parses_direction_arguments() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
@@ -517,4 +585,112 @@ mod tests {
|
|||||||
assert!(workspace_arg(&json!({ "index": 0 })).is_err());
|
assert!(workspace_arg(&json!({ "index": 0 })).is_err());
|
||||||
assert!(workspace_arg(&json!({ "index": -1 })).is_err());
|
assert!(workspace_arg(&json!({ "index": -1 })).is_err());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rejects_unsupported_versions_and_request_types_with_stable_codes() {
|
||||||
|
let version = validate_request(&request(VERSION + 1, "command")).unwrap_err();
|
||||||
|
let kind = validate_request(&request(VERSION, "event")).unwrap_err();
|
||||||
|
|
||||||
|
assert_eq!(version.code, "unsupported_version");
|
||||||
|
assert_eq!(kind.code, "invalid_type");
|
||||||
|
assert!(validate_request(&request(VERSION, "command")).is_ok());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn reads_same_user_peer_credentials() {
|
||||||
|
let (left, right) = UnixStream::pair().unwrap();
|
||||||
|
|
||||||
|
assert_eq!(peer_uid(&left).unwrap(), effective_uid());
|
||||||
|
assert_eq!(peer_uid(&right).unwrap(), effective_uid());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn malformed_ipc_connection_is_closed_without_reaching_the_request_queue() {
|
||||||
|
let (mut client, server) = UnixStream::pair().unwrap();
|
||||||
|
let (sender, _receiver) = channel::sync_channel(1);
|
||||||
|
let worker = thread::spawn(move || serve_connection(server, sender));
|
||||||
|
let invalid = b"not-json";
|
||||||
|
|
||||||
|
client
|
||||||
|
.write_all(&(invalid.len() as u32).to_be_bytes())
|
||||||
|
.unwrap();
|
||||||
|
client.write_all(invalid).unwrap();
|
||||||
|
client.shutdown(Shutdown::Write).unwrap();
|
||||||
|
client
|
||||||
|
.set_read_timeout(Some(std::time::Duration::from_secs(1)))
|
||||||
|
.unwrap();
|
||||||
|
let mut byte = [0_u8; 1];
|
||||||
|
|
||||||
|
assert_eq!(client.read(&mut byte).unwrap(), 0);
|
||||||
|
worker.join().unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn runtime_socket_directory_is_private() {
|
||||||
|
let root = unique_test_directory("private-parent");
|
||||||
|
let parent = root.join("ferese");
|
||||||
|
let socket = parent.join("control.sock");
|
||||||
|
|
||||||
|
fs::create_dir(&root).unwrap();
|
||||||
|
prepare_parent(&socket).unwrap();
|
||||||
|
let metadata = fs::metadata(&parent).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(metadata.permissions().mode() & 0o777, 0o700);
|
||||||
|
fs::remove_dir_all(root).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn listener_reclaims_only_a_stale_owned_socket() {
|
||||||
|
let root = unique_test_directory("stale-socket");
|
||||||
|
fs::create_dir(&root).unwrap();
|
||||||
|
let socket = root.join("control.sock");
|
||||||
|
let first = bind_listener(&socket).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
bind_listener(&socket).unwrap_err().kind(),
|
||||||
|
io::ErrorKind::AddrInUse
|
||||||
|
);
|
||||||
|
drop(first);
|
||||||
|
let replacement = bind_listener(&socket).unwrap();
|
||||||
|
|
||||||
|
drop(replacement);
|
||||||
|
fs::remove_file(socket).unwrap();
|
||||||
|
fs::remove_dir(root).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn listener_never_replaces_a_non_socket_path() {
|
||||||
|
let root = unique_test_directory("non-socket");
|
||||||
|
fs::create_dir(&root).unwrap();
|
||||||
|
let socket = root.join("control.sock");
|
||||||
|
fs::write(&socket, b"keep").unwrap();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
bind_listener(&socket).unwrap_err().kind(),
|
||||||
|
io::ErrorKind::AddrInUse
|
||||||
|
);
|
||||||
|
assert_eq!(fs::read(&socket).unwrap(), b"keep");
|
||||||
|
fs::remove_dir_all(root).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn ipc_worker_count_is_bounded_and_permits_are_reusable() {
|
||||||
|
let active = Arc::new(AtomicUsize::new(0));
|
||||||
|
let mut permits = (0..MAX_CONNECTIONS)
|
||||||
|
.map(|_| try_acquire_connection(&active).unwrap())
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
assert!(try_acquire_connection(&active).is_none());
|
||||||
|
permits.pop();
|
||||||
|
assert_eq!(active.load(Ordering::Acquire), MAX_CONNECTIONS - 1);
|
||||||
|
assert!(try_acquire_connection(&active).is_some());
|
||||||
|
}
|
||||||
|
|
||||||
|
fn unique_test_directory(label: &str) -> PathBuf {
|
||||||
|
let nonce = std::time::SystemTime::now()
|
||||||
|
.duration_since(std::time::UNIX_EPOCH)
|
||||||
|
.unwrap()
|
||||||
|
.as_nanos();
|
||||||
|
env::temp_dir().join(format!("ferese-ipc-{label}-{}-{nonce}", std::process::id()))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1347,6 +1347,10 @@ Events include `window-opened`, `window-closed`, `window-focused`,
|
|||||||
Subscribers have bounded queues. The server MUST disconnect a subscriber whose
|
Subscribers have bounded queues. The server MUST disconnect a subscriber whose
|
||||||
queue overflows rather than block the compositor.
|
queue overflows rather than block the compositor.
|
||||||
|
|
||||||
|
The IPC server MUST bound concurrent connection workers as well as queued
|
||||||
|
requests. Reaching either limit rejects the excess client without blocking the
|
||||||
|
compositor event loop or allocating another worker thread.
|
||||||
|
|
||||||
## 14. Input
|
## 14. Input
|
||||||
|
|
||||||
Ferese MUST support click-to-focus, configurable focus-follows-pointer, floating
|
Ferese MUST support click-to-focus, configurable focus-follows-pointer, floating
|
||||||
|
|||||||
@@ -2,9 +2,11 @@
|
|||||||
|
|
||||||
The M6 soak runner repeatedly creates and destroys native Wayland clients while
|
The M6 soak runner repeatedly creates and destroys native Wayland clients while
|
||||||
exercising focus, resize, column width, fullscreen, floating, and workspace
|
exercising focus, resize, column width, fullscreen, floating, and workspace
|
||||||
paths. Every tenth iteration also terminates a client abruptly. The runner
|
paths. Every tenth iteration also terminates a client abruptly and opens three
|
||||||
checks that the compositor and authenticated IPC socket remain responsive and
|
isolated connections that send malformed Wayland messages. The runner requires
|
||||||
records compositor RSS growth.
|
Ferese to disconnect each malformed client and then checks that the compositor
|
||||||
|
and authenticated IPC socket remain responsive. It also records compositor RSS
|
||||||
|
growth.
|
||||||
|
|
||||||
Build Ferese and `feresectl`, start the compositor, and run:
|
Build Ferese and `feresectl`, start the compositor, and run:
|
||||||
|
|
||||||
@@ -30,12 +32,24 @@ FERESE_SOAK_SECONDS=300 \
|
|||||||
./scripts/soak-native.sh
|
./scripts/soak-native.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
|
The malformed-client check can also be run independently against a disposable
|
||||||
|
Ferese session:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
WAYLAND_DISPLAY=wayland-1 cargo run -p ferese-malformed-client
|
||||||
|
target/debug/feresectl get-outputs
|
||||||
|
```
|
||||||
|
|
||||||
|
Do not point the malformed-client probe at the host compositor socket.
|
||||||
|
|
||||||
Environment controls:
|
Environment controls:
|
||||||
|
|
||||||
- `FERESE_SOAK_SECONDS`: run duration, default `86400`;
|
- `FERESE_SOAK_SECONDS`: run duration, default `86400`;
|
||||||
- `FERESE_SOAK_DELAY`: delay between operations, default `0.15` seconds;
|
- `FERESE_SOAK_DELAY`: delay between operations, default `0.15` seconds;
|
||||||
- `FERESE_SOAK_CLIENT`: native client executable, default `foot`;
|
- `FERESE_SOAK_CLIENT`: native client executable, default `foot`;
|
||||||
- `FERESECTL`: path to `feresectl`, default `target/debug/feresectl`;
|
- `FERESECTL`: path to `feresectl`, default `target/debug/feresectl`;
|
||||||
|
- `FERESE_MALFORMED_CLIENT`: path to the malformed protocol probe, default
|
||||||
|
`target/debug/ferese-malformed-client`;
|
||||||
- `FERESE_PID`: compositor PID; automatic lookup is only a fallback;
|
- `FERESE_PID`: compositor PID; automatic lookup is only a fallback;
|
||||||
- `FERESE_SOAK_LOG`: result log, default `/tmp/ferese-soak-<pid>.log`.
|
- `FERESE_SOAK_LOG`: result log, default `/tmp/ferese-soak-<pid>.log`.
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ soak_seconds="${FERESE_SOAK_SECONDS:-86400}"
|
|||||||
iteration_delay="${FERESE_SOAK_DELAY:-0.15}"
|
iteration_delay="${FERESE_SOAK_DELAY:-0.15}"
|
||||||
client_program="${FERESE_SOAK_CLIENT:-foot}"
|
client_program="${FERESE_SOAK_CLIENT:-foot}"
|
||||||
feresectl_program="${FERESECTL:-target/debug/feresectl}"
|
feresectl_program="${FERESECTL:-target/debug/feresectl}"
|
||||||
|
malformed_client_program="${FERESE_MALFORMED_CLIENT:-target/debug/ferese-malformed-client}"
|
||||||
compositor_pid="${FERESE_PID:-}"
|
compositor_pid="${FERESE_PID:-}"
|
||||||
log_path="${FERESE_SOAK_LOG:-/tmp/ferese-soak-$$.log}"
|
log_path="${FERESE_SOAK_LOG:-/tmp/ferese-soak-$$.log}"
|
||||||
|
|
||||||
@@ -23,6 +24,10 @@ if [[ ! -x "$feresectl_program" ]]; then
|
|||||||
cargo build -p feresectl
|
cargo build -p feresectl
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
if [[ ! -x "$malformed_client_program" ]]; then
|
||||||
|
cargo build -p ferese-malformed-client
|
||||||
|
fi
|
||||||
|
|
||||||
if ! command -v "$client_program" >/dev/null 2>&1; then
|
if ! command -v "$client_program" >/dev/null 2>&1; then
|
||||||
echo "soak client not found: $client_program" >&2
|
echo "soak client not found: $client_program" >&2
|
||||||
exit 2
|
exit 2
|
||||||
@@ -135,6 +140,8 @@ while ((SECONDS < deadline)); do
|
|||||||
launch_client
|
launch_client
|
||||||
wait_for_focus
|
wait_for_focus
|
||||||
kill -TERM "${client_pids[-1]}" 2>/dev/null || true
|
kill -TERM "${client_pids[-1]}" 2>/dev/null || true
|
||||||
|
"$malformed_client_program" >>"$log_path" 2>&1
|
||||||
|
control get-outputs
|
||||||
fi
|
fi
|
||||||
reap_clients
|
reap_clients
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
[package]
|
||||||
|
name = "ferese-malformed-client"
|
||||||
|
version.workspace = true
|
||||||
|
edition.workspace = true
|
||||||
|
license.workspace = true
|
||||||
|
rust-version.workspace = true
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
@@ -0,0 +1,150 @@
|
|||||||
|
use std::env;
|
||||||
|
use std::error::Error;
|
||||||
|
use std::io::{self, Read, Write};
|
||||||
|
use std::net::Shutdown;
|
||||||
|
use std::os::unix::net::UnixStream;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
const DISCONNECT_TIMEOUT: Duration = Duration::from_secs(2);
|
||||||
|
const UNKNOWN_OBJECT_FRAME: [u8; 8] = frame_header(u32::MAX, 0, 8);
|
||||||
|
const UNDERSIZED_FRAME: [u8; 8] = frame_header(1, 0, 4);
|
||||||
|
const UNALIGNED_FRAME: [u8; 9] = {
|
||||||
|
let header = frame_header(1, 0, 9);
|
||||||
|
[
|
||||||
|
header[0], header[1], header[2], header[3], header[4], header[5], header[6], header[7], 0,
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
struct MalformedCase {
|
||||||
|
name: &'static str,
|
||||||
|
bytes: &'static [u8],
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() -> Result<(), Box<dyn Error>> {
|
||||||
|
let socket = display_socket()?;
|
||||||
|
let cases = malformed_cases();
|
||||||
|
|
||||||
|
for case in cases {
|
||||||
|
exercise_case(&socket, &case)?;
|
||||||
|
println!("PASS {}", case.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn display_socket() -> Result<PathBuf, io::Error> {
|
||||||
|
let display = env::var_os("WAYLAND_DISPLAY")
|
||||||
|
.filter(|value| !value.is_empty())
|
||||||
|
.ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "WAYLAND_DISPLAY is not set"))?;
|
||||||
|
let display = PathBuf::from(display);
|
||||||
|
if display.is_absolute() {
|
||||||
|
return Ok(display);
|
||||||
|
}
|
||||||
|
|
||||||
|
env::var_os("XDG_RUNTIME_DIR")
|
||||||
|
.filter(|value| !value.is_empty())
|
||||||
|
.map(PathBuf::from)
|
||||||
|
.map(|directory| directory.join(display))
|
||||||
|
.ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "XDG_RUNTIME_DIR is not set"))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn malformed_cases() -> [MalformedCase; 3] {
|
||||||
|
[
|
||||||
|
MalformedCase {
|
||||||
|
name: "unknown-object",
|
||||||
|
bytes: &UNKNOWN_OBJECT_FRAME,
|
||||||
|
},
|
||||||
|
MalformedCase {
|
||||||
|
name: "undersized-frame",
|
||||||
|
bytes: &UNDERSIZED_FRAME,
|
||||||
|
},
|
||||||
|
MalformedCase {
|
||||||
|
name: "unaligned-frame",
|
||||||
|
bytes: &UNALIGNED_FRAME,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
const fn frame_header(object: u32, opcode: u16, size: u16) -> [u8; 8] {
|
||||||
|
let object = object.to_ne_bytes();
|
||||||
|
let size_and_opcode = ((size as u32) << 16 | opcode as u32).to_ne_bytes();
|
||||||
|
[
|
||||||
|
object[0],
|
||||||
|
object[1],
|
||||||
|
object[2],
|
||||||
|
object[3],
|
||||||
|
size_and_opcode[0],
|
||||||
|
size_and_opcode[1],
|
||||||
|
size_and_opcode[2],
|
||||||
|
size_and_opcode[3],
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn exercise_case(socket: &PathBuf, case: &MalformedCase) -> Result<(), Box<dyn Error>> {
|
||||||
|
let mut stream = UnixStream::connect(socket)?;
|
||||||
|
stream.set_read_timeout(Some(DISCONNECT_TIMEOUT))?;
|
||||||
|
stream.write_all(case.bytes)?;
|
||||||
|
stream.shutdown(Shutdown::Write)?;
|
||||||
|
|
||||||
|
let mut response = [0_u8; 256];
|
||||||
|
loop {
|
||||||
|
match stream.read(&mut response) {
|
||||||
|
Ok(0) => return Ok(()),
|
||||||
|
Ok(_) => continue,
|
||||||
|
Err(error)
|
||||||
|
if matches!(
|
||||||
|
error.kind(),
|
||||||
|
io::ErrorKind::ConnectionReset | io::ErrorKind::BrokenPipe
|
||||||
|
) =>
|
||||||
|
{
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
Err(error)
|
||||||
|
if matches!(
|
||||||
|
error.kind(),
|
||||||
|
io::ErrorKind::WouldBlock | io::ErrorKind::TimedOut
|
||||||
|
) =>
|
||||||
|
{
|
||||||
|
return Err(format!(
|
||||||
|
"compositor did not disconnect malformed case {:?} within {:?}",
|
||||||
|
case.name, DISCONNECT_TIMEOUT
|
||||||
|
)
|
||||||
|
.into());
|
||||||
|
}
|
||||||
|
Err(error) => return Err(error.into()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn encodes_native_wayland_headers() {
|
||||||
|
let header = UNKNOWN_OBJECT_FRAME;
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
u32::from_ne_bytes(header[..4].try_into().unwrap()),
|
||||||
|
u32::MAX
|
||||||
|
);
|
||||||
|
assert_eq!(u32::from_ne_bytes(header[4..].try_into().unwrap()), 8 << 16);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn malformed_sizes_match_the_transmitted_byte_counts() {
|
||||||
|
let undersized = UNDERSIZED_FRAME;
|
||||||
|
let unaligned = UNALIGNED_FRAME;
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
u32::from_ne_bytes(undersized[4..].try_into().unwrap()) >> 16,
|
||||||
|
4
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
u32::from_ne_bytes(unaligned[4..8].try_into().unwrap()) >> 16,
|
||||||
|
9
|
||||||
|
);
|
||||||
|
assert_eq!(unaligned.len(), 9);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in new issue
Block a user