Skip to content

Malformed UDP packet flooding starves dp_event_loop, causing all DataWriter::write() calls to return persistent WouldBlock (crash-free DoS) #406

@TUPYP7180

Description

@TUPYP7180

Summary

Component: src/network/udp_listener.rs, src/rtps/dp_event_loop.rs, src/dds/helpers.rs

RustDDS runs all RTPS processing in a single-threaded event loop (dp_event_loop). When the loop receives a UDP event, it reads all pending datagrams from the socket in one batch (UDPListener::messages()) and processes them sequentially before returning control. If an attacker floods the target with malformed UDP packets, the batch processing occupies the event loop thread for longer than the DataWriter channel timeout (20 ms for BestEffort QoS, 10 ms for built-in Discovery writers). Every subsequent DataWriter::write() call returns WouldBlock, and the node stops publishing entirely.

The process does not crash, no Rust panic is raised, and no ASAN alert fires. From the outside the node appears to be running, but it silently stops producing any data — a crash-free, low-visibility denial of service.

Affected Version

  • RustDDS: v0.11.9
  • OS: Ubuntu 22.04 (Linux 6.8.0, x86_64)
  • Compiler: Rust stable (no sanitizer)

Root Cause

1. Unbounded batch read in UDPListener::messages() (src/network/udp_listener.rs:198)

pub fn messages(&mut self) -> Vec<Bytes> {
    let mut messages = Vec::with_capacity(4);
    loop {
        let nbytes = match self.socket.recv(&mut self.receive_buffer) {
            Ok(n) => n,
            Err(e) if e.kind() == io::ErrorKind::WouldBlock => return messages,
            Err(_) => return messages,
        };
        messages.push(Bytes::copy_from_slice(&self.receive_buffer[..nbytes]));
    }
    // reads until the socket buffer is empty — no per-poll packet cap
}

All queued packets are drained before the function returns, so a large OS receive buffer backlog translates directly into a long synchronous loop in the event thread.

2. Single-threaded event loop processes UDP before writer commands (src/rtps/dp_event_loop.rs:316)

let udp_messages = ev_wrapper
    .udp_listeners.get_mut(&event.token())
    .map_or_else(..., UDPListener::messages);

for packet in udp_messages {
    ev_wrapper.message_receiver.handle_received_packet(&packet);
}
// writer_command_receiver is not checked until the next poll() call

3. cc_upload channel capacity is 16; writer timeout is 20 ms (src/dds/pubsub.rs:459, src/dds/helpers.rs:10)

let (dwcc_upload, hccc_download) = mio_channel::sync_channel::<WriterCommand>(16);

pub const TIMEOUT_FALLBACK: Duration = Duration::from_nanos(20_000_000); // 20 ms

If the event loop is busy processing UDP packets for more than 20 ms, the writer channel fills up and DataWriter::write() returns WouldBlock.

4. Built-in Discovery writer has an even tighter timeout (10 ms, src/rtps/discovery.rs:2004)

DCPSParticipantMessage (liveliness heartbeat) uses a 10 ms blocking limit. When this writer starves, remote participants stop receiving heartbeats and eventually expel the affected node from the DDS network — even though the process is still running and the network is reachable.

Observed Behavior

The following was observed during testing with a node in a multi-participant DDS domain while another participant was generating high volumes of malformed RTPS UDP traffic.

Timeline (node p5):

10:36:11  p5 starts; two DataWriters running normally (one write every 3 s)
10:36:11  Malformed RTPS packets begin arriving; deserialization warnings appear
10:38:50  seq=54 (~162 s later): WouldBlock first appears; recurs on every write attempt
10:39:51  Last log entry (seq=74); node effectively dead

Crash log excerpt (rustdds_p5_stderr.log):

[2026-04-09T10:36:12Z WARN  rustdds::rtps::message_receiver]
    RTPS deserialize error Custom { kind: InvalidInput,
    error: "Submessage header declares length larger than remaining message size: 4 + 28 <= 28" }

[2026-04-09T10:38:50Z WARN  rustdds::dds::with_key::datawriter]
    Write timed out: topic="diagnostic/cpu"  timeout=None

[RustDDS] p5: write error on '6_1_1': WouldBlock { data: FuzzData { seq: 54, ... } }
[RustDDS] p5: write error on '6_2_1': WouldBlock { data: FuzzData { seq: 54, ... } }
... (repeats every 3 s until process is killed)

Observation with DCPSParticipantMessage (node p3):

[ERROR rustdds::rtps::rtps_reader_proxy]
    all_acked_before updated backwards!   ← also present, independent bug
[WARN  rustdds::dds::with_key::datawriter]
    Write timed out: topic="DCPSParticipantMessage"  timeout=Some(0.009999999 sec)
                                          ← liveliness heartbeat fails first (10 ms limit)
... (user topics follow ~35 s later)

Once DCPSParticipantMessage starts returning WouldBlock, other participants stop receiving liveliness heartbeats and disassociate from the affected node, causing a network-level split even though the process remains alive.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions