Skip to content

Commit 8ca0161

Browse files
committed
Fix CVE-2025-64438
This commit fixes CVE-2025-64438 by limiting the number of accepted GAPs that can be stored in the internal set of `WriterProxy`. This is an squashed commit of a privately reviewed branch. Signed-off-by: Miguel Company <[email protected]> Reviewed-by: cferreiragonz <[email protected]>
1 parent 0c3824e commit 8ca0161

File tree

4 files changed

+383
-38
lines changed

4 files changed

+383
-38
lines changed

src/cpp/rtps/reader/StatefulReader.cpp

Lines changed: 6 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -929,47 +929,23 @@ bool StatefulReader::process_gap_msg(
929929

930930
if (acceptMsgFrom(writerGUID, &pWP) && pWP)
931931
{
932-
// TODO (Miguel C): Refactor this inside WriterProxy
933-
SequenceNumber_t auxSN;
934-
SequenceNumber_t finalSN = gapList.base();
935932
History::const_iterator history_iterator = history_->changesBegin();
936-
for (auxSN = gapStart; auxSN < finalSN; auxSN++)
937-
{
938-
if (pWP->irrelevant_change_set(auxSN))
939-
{
940-
CacheChange_t* to_remove = nullptr;
941-
auto ret_iterator = find_cache_in_fragmented_process(auxSN, pWP->guid(), to_remove, history_iterator);
942-
if (to_remove != nullptr)
943-
{
944-
// we called the History version to avoid callbacks
945-
history_iterator = history_->History::remove_change_nts(ret_iterator);
946-
}
947-
else if (ret_iterator != history_->changesEnd())
948-
{
949-
history_iterator = ret_iterator;
950-
}
951-
}
952-
}
953-
954-
gapList.for_each(
955-
[&](SequenceNumber_t it)
956-
{
957-
if (pWP->irrelevant_change_set(it))
933+
auto remove_fn = [this, &writerGUID, &history_iterator](const SequenceNumber_t& seq)
958934
{
959935
CacheChange_t* to_remove = nullptr;
960-
auto ret_iterator =
961-
find_cache_in_fragmented_process(auxSN, pWP->guid(), to_remove, history_iterator);
936+
auto ret_iterator = find_cache_in_fragmented_process(seq, writerGUID, to_remove, history_iterator);
962937
if (to_remove != nullptr)
963938
{
964-
// we called the History version to avoid callbacks
939+
// we call the History version to avoid callbacks
965940
history_iterator = history_->History::remove_change_nts(ret_iterator);
966941
}
967942
else if (ret_iterator != history_->changesEnd())
968943
{
969944
history_iterator = ret_iterator;
970945
}
971-
}
972-
});
946+
};
947+
948+
pWP->process_gap(gapStart, gapList, remove_fn);
973949

974950
// Maybe now we have to notify user from new CacheChanges.
975951
NotifyChanges(pWP);

src/cpp/rtps/reader/WriterProxy.h

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
#define FASTDDS_RTPS_READER_WRITERPROXY_H_
2121
#ifndef DOXYGEN_SHOULD_SKIP_THIS_PUBLIC
2222

23+
#include <algorithm>
2324
#include <set>
2425
#include <vector>
2526

@@ -143,6 +144,74 @@ class WriterProxy : public RTPSMessageSenderInterface
143144
bool irrelevant_change_set(
144145
const SequenceNumber_t& seq_num);
145146

147+
/**
148+
* Process a gap received from the writer.
149+
*
150+
* @param gap_start Sequence number as received in the GAP message.
151+
* @param gap_list Sequence number set as received in the GAP message.
152+
* @param remove_fn Function to be called for each irrelevant change found.
153+
*/
154+
template<typename Func>
155+
inline void process_gap(
156+
const SequenceNumber_t& gap_start,
157+
const SequenceNumberSet_t& gap_list,
158+
Func&& remove_fn)
159+
{
160+
// First sequence number that can be considered for GAP processing
161+
SequenceNumber_t first_allowed_gap = changes_from_writer_low_mark_ + 1u;
162+
163+
// Cap start sequence number
164+
SequenceNumber_t initial_seq = std::max(gap_start, first_allowed_gap);
165+
166+
// Default maximum allowed GAP is low mark + 256
167+
SequenceNumber_t max_allowed_gap = changes_from_writer_low_mark_ + 256u;
168+
169+
// Special case when initial_seq is exactly the first allowed GAP
170+
if (initial_seq == first_allowed_gap)
171+
{
172+
max_allowed_gap = gap_list.base() + 256u;
173+
174+
// Do not exceed max sequence number when known from a heartbeat
175+
if (max_sequence_number_ > changes_from_writer_low_mark_)
176+
{
177+
max_allowed_gap = max_sequence_number_ + 1;
178+
}
179+
}
180+
181+
// Early exit if gap_start is beyond allowed range
182+
if (gap_start > max_allowed_gap)
183+
{
184+
return;
185+
}
186+
187+
// Iterate through all sequence numbers in [initial_seq, final_seq)
188+
SequenceNumber_t auxSN;
189+
SequenceNumber_t finalSN = std::min(gap_list.base(), max_allowed_gap);
190+
for (auxSN = initial_seq; auxSN < finalSN; auxSN++)
191+
{
192+
if (irrelevant_change_set(auxSN))
193+
{
194+
remove_fn(auxSN);
195+
}
196+
}
197+
198+
// Early exit if the entire gap_list is beyond allowed range
199+
if (gap_list.base() > max_allowed_gap)
200+
{
201+
return;
202+
}
203+
204+
// Iterate through all sequence numbers in the gap_list
205+
gap_list.for_each(
206+
[&](SequenceNumber_t it)
207+
{
208+
if ((it < max_allowed_gap) && irrelevant_change_set(it))
209+
{
210+
remove_fn(it);
211+
}
212+
});
213+
}
214+
146215
/**
147216
* Check if this proxy has any missing change.
148217
* @return true when there is at least one missing change on this proxy.

test/blackbox/common/BlackboxTestsTransportUDP.cpp

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -808,6 +808,89 @@ TEST(TransportUDP, KeyOnlyBigPayloadIgnored_BestEffort)
808808
KeyOnlyBigPayloadIgnored(writer, reader);
809809
}
810810

811+
// Regression test for redmine issue #23829
812+
TEST(TransportUDP, MaliciousGapBigRange)
813+
{
814+
// Force using UDP transport
815+
auto udp_transport = std::make_shared<UDPv4TransportDescriptor>();
816+
817+
PubSubWriter<UnboundedHelloWorldPubSubType> writer(TEST_TOPIC_NAME);
818+
PubSubReader<UnboundedHelloWorldPubSubType> reader(TEST_TOPIC_NAME);
819+
820+
struct MaliciousGapBigRange
821+
{
822+
std::array<char, 4> rtps_id{ {'R', 'T', 'P', 'S'} };
823+
std::array<uint8_t, 2> protocol_version{ {2, 3} };
824+
std::array<uint8_t, 2> vendor_id{ {0x01, 0x0F} };
825+
GuidPrefix_t sender_prefix{};
826+
827+
struct GapSubMsg
828+
{
829+
uint8_t submessage_id = 0x08;
830+
#if FASTDDS_IS_BIG_ENDIAN_TARGET
831+
uint8_t flags = 0x00;
832+
#else
833+
uint8_t flags = 0x01;
834+
#endif // FASTDDS_IS_BIG_ENDIAN_TARGET
835+
uint16_t octets_to_next_header = 28;
836+
EntityId_t reader_id{};
837+
EntityId_t writer_id{};
838+
SequenceNumber_t gap_start{ 10u };
839+
SequenceNumber_t gap_list_base{ std::numeric_limits<uint32_t>::max() };
840+
uint32_t num_longs_bitmap = 0;
841+
} gap;
842+
};
843+
844+
UDPMessageSender fake_msg_sender;
845+
846+
// Set common QoS
847+
reader.disable_builtin_transport().add_user_transport_to_pparams(udp_transport)
848+
.history_depth(10).reliability(eprosima::fastdds::dds::RELIABLE_RELIABILITY_QOS);
849+
writer.history_depth(10).reliability(eprosima::fastdds::dds::RELIABLE_RELIABILITY_QOS);
850+
851+
// Set custom reader locator so we can send malicious data to a known location
852+
Locator_t reader_locator;
853+
ASSERT_TRUE(IPLocator::setIPv4(reader_locator, "127.0.0.1"));
854+
reader_locator.port = 7000;
855+
reader.add_to_unicast_locator_list("127.0.0.1", 7000);
856+
857+
// Initialize and wait for discovery
858+
reader.init();
859+
ASSERT_TRUE(reader.isInitialized());
860+
writer.init();
861+
ASSERT_TRUE(writer.isInitialized());
862+
863+
reader.wait_discovery();
864+
writer.wait_discovery();
865+
866+
// Send 1 sample and wait for reception
867+
auto data = default_unbounded_helloworld_data_generator(1);
868+
reader.startReception(data);
869+
writer.send(data);
870+
ASSERT_TRUE(data.empty());
871+
reader.block_for_all();
872+
873+
// Send malicious data
874+
{
875+
auto writer_guid = writer.datawriter_guid();
876+
877+
MaliciousGapBigRange malicious_packet{};
878+
malicious_packet.sender_prefix = writer_guid.guidPrefix;
879+
malicious_packet.gap.writer_id = writer_guid.entityId;
880+
malicious_packet.gap.reader_id = reader.datareader_guid().entityId;
881+
882+
CDRMessage_t msg(0);
883+
uint32_t msg_len = static_cast<uint32_t>(sizeof(malicious_packet));
884+
msg.init(reinterpret_cast<octet*>(&malicious_packet), msg_len);
885+
msg.length = msg_len;
886+
msg.pos = msg_len;
887+
fake_msg_sender.send(msg, reader_locator);
888+
}
889+
890+
// Block for some time to let the message be processed
891+
std::this_thread::sleep_for(std::chrono::milliseconds(500));
892+
}
893+
811894
// Regression test for redmine issue #23830
812895
TEST(TransportUDP, MaliciousDataFragUnalignedSizes)
813896
{

0 commit comments

Comments
 (0)