Revert "Fix data channel message integrity violation"

This reverts commit 6cbff752f52bf3f70168d551c33ce719bd8e0663.

Reason for revert: breaking downstream projects, Win MSVC x86 dbg and Win x86 Clang rel

Original change's description:
> Fix data channel message integrity violation
> 
> SCTP message chunks and notifications are being delivered interleaved.
> However, the way the code was structured previously, a notification
> would interrupt reassembly of a message chunk and hand out the partial
> message, thereby violating message integrity. This patch separates the
> handling of notifications and reassembly of messages.
> 
> Additional changes:
> 
> - Remove illegal cast from non-validated u32 to enum (PPID)
> - Drop partial messages if the SID has been changed but EOR not yet
>   received instead of delivering them. (This should never happen
>   anyway.)
> - Don't treat TSN as timestamp (wat)
> 
> Bug: webrtc:11708
> Change-Id: I4e2fe2262feda2a96d2ae3f6ce9b06370d9878ae
> Reviewed-on: https://webrtc-review.googlesource.com/c/src/+/177527
> Reviewed-by: Karl Wiberg <kwiberg@webrtc.org>
> Reviewed-by: Taylor <deadbeef@webrtc.org>
> Reviewed-by: Harald Alvestrand <hta@webrtc.org>
> Commit-Queue: Tommi <tommi@webrtc.org>
> Cr-Commit-Position: refs/heads/master@{#31605}

TBR=deadbeef@webrtc.org,kwiberg@webrtc.org,tommi@webrtc.org,hta@webrtc.org,lennart.grahl@gmail.com

Change-Id: I6d6c5a11835f155f8c449b996d034f43b8db452c
No-Presubmit: true
No-Tree-Checks: true
No-Try: true
Bug: webrtc:11708
Reviewed-on: https://webrtc-review.googlesource.com/c/src/+/178488
Reviewed-by: Alessio Bazzica <alessiob@webrtc.org>
Commit-Queue: Alessio Bazzica <alessiob@webrtc.org>
Cr-Commit-Position: refs/heads/master@{#31606}
This commit is contained in:
Alessio Bazzica 2020-07-01 14:45:15 +00:00 committed by Commit Bot
parent 6cbff752f5
commit 51e08b8c19
5 changed files with 103 additions and 209 deletions

View File

@ -90,7 +90,6 @@ CZ Theng <cz.theng@gmail.com>
Miguel Paris <mparisdiaz@gmail.com>
Raman Budny <budnyjj@gmail.com>
Stephan Hartmann <stha09@googlemail.com>
Lennart Grahl <lennart.grahl@gmail.com>
&yet LLC <*@andyet.com>
8x8 Inc. <*@sip-communicator.org>

View File

@ -659,14 +659,5 @@ if (rtc_include_tests) {
if (is_ios) {
deps += [ ":rtc_media_unittests_bundle_data" ]
}
if (rtc_enable_sctp && rtc_build_usrsctp) {
include_dirs = [
# TODO(jiayl): move this into the public_configs of
# //third_party/usrsctp/BUILD.gn.
"//third_party/usrsctp/usrsctplib",
]
deps += [ "//third_party/usrsctp" ]
}
}
}

View File

@ -62,7 +62,7 @@ ABSL_CONST_INIT rtc::GlobalLock g_usrsctp_lock_;
// http://www.iana.org/assignments/sctp-parameters/sctp-parameters.xml
// The value is not used by SCTP itself. It indicates the protocol running
// on top of SCTP.
enum {
enum PayloadProtocolIdentifier {
PPID_NONE = 0, // No protocol is specified.
// Matches the PPIDs in mozilla source and
// https://datatracker.ietf.org/doc/draft-ietf-rtcweb-data-protocol Sec. 9
@ -143,7 +143,7 @@ void DebugSctpPrintf(const char* format, ...) {
}
// Get the PPID to use for the terminating fragment of this type.
uint32_t GetPpid(cricket::DataMessageType type) {
PayloadProtocolIdentifier GetPpid(cricket::DataMessageType type) {
switch (type) {
default:
case cricket::DMT_NONE:
@ -157,7 +157,8 @@ uint32_t GetPpid(cricket::DataMessageType type) {
}
}
bool GetDataMediaType(uint32_t ppid, cricket::DataMessageType* dest) {
bool GetDataMediaType(PayloadProtocolIdentifier ppid,
cricket::DataMessageType* dest) {
RTC_DCHECK(dest != NULL);
switch (ppid) {
case PPID_BINARY_PARTIAL:
@ -381,10 +382,77 @@ class SctpTransport::UsrSctpWrapper {
int flags,
void* ulp_info) {
SctpTransport* transport = static_cast<SctpTransport*>(ulp_info);
int result =
transport->OnDataOrNotificationFromSctp(data, length, rcv, flags);
free(data);
return result;
// Post data to the transport's receiver thread (copying it).
// TODO(ldixon): Unclear if copy is needed as this method is responsible for
// memory cleanup. But this does simplify code.
const PayloadProtocolIdentifier ppid =
static_cast<PayloadProtocolIdentifier>(
rtc::NetworkToHost32(rcv.rcv_ppid));
DataMessageType type = DMT_NONE;
if (!GetDataMediaType(ppid, &type) && !(flags & MSG_NOTIFICATION)) {
// It's neither a notification nor a recognized data packet. Drop it.
RTC_LOG(LS_ERROR) << "Received an unknown PPID " << ppid
<< " on an SCTP packet. Dropping.";
free(data);
} else {
ReceiveDataParams params;
params.sid = rcv.rcv_sid;
params.seq_num = rcv.rcv_ssn;
params.timestamp = rcv.rcv_tsn;
params.type = type;
// Expect only continuation messages belonging to the same sid, the sctp
// stack should ensure this.
if ((transport->partial_incoming_message_.size() != 0) &&
(rcv.rcv_sid != transport->partial_params_.sid)) {
// A message with a new sid, but haven't seen the EOR for the
// previous message. Deliver the previous partial message to avoid
// merging messages from different sid's.
transport->invoker_.AsyncInvoke<void>(
RTC_FROM_HERE, transport->network_thread_,
rtc::Bind(&SctpTransport::OnInboundPacketFromSctpToTransport,
transport, transport->partial_incoming_message_,
transport->partial_params_, transport->partial_flags_));
transport->partial_incoming_message_.Clear();
}
transport->partial_incoming_message_.AppendData(
reinterpret_cast<uint8_t*>(data), length);
transport->partial_params_ = params;
transport->partial_flags_ = flags;
free(data);
// Merge partial messages until they exceed the maximum send buffer size.
// This enables messages from a single send to be delivered in a single
// callback. Larger messages (originating from other implementations) will
// still be delivered in chunks.
if (!(flags & MSG_EOR) &&
(transport->partial_incoming_message_.size() < kSctpSendBufferSize)) {
return 1;
}
if (!(flags & MSG_EOR)) {
// TODO(bugs.webrtc.org/7774): We currently chunk messages if they are
// >= kSctpSendBufferSize. The better thing to do here is buffer up to
// the size negotiated in the SDP, and if a larger message is received
// close the channel and report the error. See discussion in the bug.
RTC_LOG(LS_WARNING) << "Chunking SCTP message without the EOR bit set.";
}
// The ownership of the packet transfers to |invoker_|. Using
// CopyOnWriteBuffer is the most convenient way to do this.
transport->invoker_.AsyncInvoke<void>(
RTC_FROM_HERE, transport->network_thread_,
rtc::Bind(&SctpTransport::OnInboundPacketFromSctpToTransport,
transport, transport->partial_incoming_message_, params,
flags));
transport->partial_incoming_message_.Clear();
}
return 1;
}
static SctpTransport* GetTransportFromSocket(struct socket* sock) {
@ -1064,120 +1132,31 @@ void SctpTransport::OnPacketFromSctpToNetwork(
rtc::PacketOptions(), PF_NORMAL);
}
int SctpTransport::InjectDataOrNotificationFromSctpForTesting(
void* data,
size_t length,
struct sctp_rcvinfo rcv,
void SctpTransport::OnInboundPacketFromSctpToTransport(
const rtc::CopyOnWriteBuffer& buffer,
ReceiveDataParams params,
int flags) {
return OnDataOrNotificationFromSctp(data, length, rcv, flags);
}
int SctpTransport::OnDataOrNotificationFromSctp(void* data,
size_t length,
struct sctp_rcvinfo rcv,
int flags) {
// If data is NULL, the SCTP association has been closed.
if (!data) {
RTC_LOG(LS_INFO) << debug_name_
<< "->OnSctpInboundPacket(...): "
"No data, closing.";
return 1;
}
// Handle notifications early.
// Note: Notifications are never split into chunks, so they can and should
// be handled early and entirely separate from the reassembly
// process.
if (flags & MSG_NOTIFICATION) {
RTC_LOG(LS_VERBOSE) << debug_name_
<< "->OnSctpInboundPacket(...): SCTP notification"
<< " length=" << length;
// Copy and dispatch asynchronously
rtc::CopyOnWriteBuffer notification(reinterpret_cast<uint8_t*>(data),
length);
invoker_.AsyncInvoke<void>(
RTC_FROM_HERE, network_thread_,
rtc::Bind(&SctpTransport::OnNotificationFromSctp, this, notification));
return 1;
}
// Log data chunk
const uint32_t ppid = rtc::NetworkToHost32(rcv.rcv_ppid);
RTC_DCHECK_RUN_ON(network_thread_);
RTC_LOG(LS_VERBOSE) << debug_name_
<< "->OnSctpInboundPacket(...): SCTP data chunk"
<< " length=" << length << ", sid=" << rcv.rcv_sid
<< ", ppid=" << ppid << ", ssn=" << rcv.rcv_ssn
<< ", cum-tsn=" << rcv.rcv_cumtsn
<< ", eor=" << ((flags & MSG_EOR) ? "y" : "n");
// Validate payload protocol identifier
DataMessageType type = DMT_NONE;
if (!GetDataMediaType(ppid, &type)) {
// Unexpected PPID, dropping
RTC_LOG(LS_ERROR) << "Received an unknown PPID " << ppid
<< " on an SCTP packet. Dropping.";
return 1;
<< "->OnInboundPacketFromSctpToTransport(...): "
"Received SCTP data:"
" sid="
<< params.sid
<< " notification: " << (flags & MSG_NOTIFICATION)
<< " length=" << buffer.size();
// Sending a packet with data == NULL (no data) is SCTPs "close the
// connection" message. This sets sock_ = NULL;
if (!buffer.size() || !buffer.data()) {
RTC_LOG(LS_INFO) << debug_name_
<< "->OnInboundPacketFromSctpToTransport(...): "
"No data, closing.";
return;
}
// Expect only continuation messages belonging to the same SID. The SCTP
// stack is expected to ensure this as long as the User Message
// Interleaving extension (RFC 8260) is not explicitly enabled, so this
// merely acts as a safeguard.
if ((partial_incoming_message_.size() != 0) &&
(rcv.rcv_sid != partial_params_.sid)) {
RTC_LOG(LS_ERROR) << "Received a new SID without EOR in the previous"
<< " SCTP packet. Discarding the previous packet.";
partial_incoming_message_.Clear();
if (flags & MSG_NOTIFICATION) {
OnNotificationFromSctp(buffer);
} else {
OnDataFromSctpToTransport(params, buffer);
}
// Copy metadata of interest
ReceiveDataParams params;
params.type = type;
params.sid = rcv.rcv_sid;
// Note that the SSN is identical for each chunk of the same message.
// Furthermore, it is increased per stream and not on the whole
// association.
params.seq_num = rcv.rcv_ssn;
// There is no timestamp field in the SCTP API
params.timestamp = 0;
// Append the chunk's data to the message buffer
partial_incoming_message_.AppendData(reinterpret_cast<uint8_t*>(data),
length);
partial_params_ = params;
partial_flags_ = flags;
// If the message is not yet complete...
if (!(flags & MSG_EOR)) {
if (partial_incoming_message_.size() < kSctpSendBufferSize) {
// We still have space in the buffer. Continue buffering chunks until
// the message is complete before handing it out.
return 1;
} else {
// The sender is exceeding the maximum message size that we announced.
// Spit out a warning but still hand out the partial message. Note that
// this behaviour is undesirable, see the discussion in issue 7774.
//
// TODO(lgrahl): Once sufficient time has passed and all supported
// browser versions obey the announced maximum message size, we should
// abort the SCTP association instead to prevent message integrity
// violation.
RTC_LOG(LS_ERROR) << "Handing out partial SCTP message.";
}
}
// Dispatch the complete message.
// The ownership of the packet transfers to |invoker_|. Using
// CopyOnWriteBuffer is the most convenient way to do this.
invoker_.AsyncInvoke<void>(
RTC_FROM_HERE, network_thread_,
rtc::Bind(&SctpTransport::OnDataFromSctpToTransport, this, params,
partial_incoming_message_));
// Reset the message buffer
partial_incoming_message_.Clear();
return 1;
}
void SctpTransport::OnDataFromSctpToTransport(

View File

@ -34,7 +34,6 @@
// Defined by "usrsctplib/usrsctp.h"
struct sockaddr_conn;
struct sctp_assoc_change;
struct sctp_rcvinfo;
struct sctp_stream_reset_event;
struct sctp_sendv_spa;
@ -59,8 +58,8 @@ struct SctpInboundPacket;
// 8. usrsctp_conninput(wrapped_data)
// [network thread returns; sctp thread then calls the following]
// 9. OnSctpInboundData(data)
// 10. SctpTransport::OnDataFromSctpToTransport(data)
// [sctp thread returns having async invoked on the network thread]
// 10. SctpTransport::OnInboundPacketFromSctpToTransport(inboundpacket)
// 11. SctpTransport::OnDataFromSctpToTransport(data)
// 12. SctpTransport::SignalDataReceived(data)
// [from the same thread, methods registered/connected to
@ -95,10 +94,6 @@ class SctpTransport : public SctpTransportInternal,
void set_debug_name_for_testing(const char* debug_name) override {
debug_name_ = debug_name;
}
int InjectDataOrNotificationFromSctpForTesting(void* data,
size_t length,
struct sctp_rcvinfo rcv,
int flags);
// Exposed to allow Post call from c-callbacks.
// TODO(deadbeef): Remove this or at least make it return a const pointer.
@ -178,16 +173,14 @@ class SctpTransport : public SctpTransportInternal,
// Called using |invoker_| to send packet on the network.
void OnPacketFromSctpToNetwork(const rtc::CopyOnWriteBuffer& buffer);
// Called on the SCTP thread
int OnDataOrNotificationFromSctp(void* data,
size_t length,
struct sctp_rcvinfo rcv,
int flags);
// Called using |invoker_| to decide what to do with the data.
// Called using |invoker_| to decide what to do with the packet.
// The |flags| parameter is used by SCTP to distinguish notification packets
// from other types of packets.
void OnInboundPacketFromSctpToTransport(const rtc::CopyOnWriteBuffer& buffer,
ReceiveDataParams params,
int flags);
void OnDataFromSctpToTransport(const ReceiveDataParams& params,
const rtc::CopyOnWriteBuffer& buffer);
// Called using |invoker_| to decide what to do with the notification.
void OnNotificationFromSctp(const rtc::CopyOnWriteBuffer& buffer);
void OnNotificationAssocChange(const sctp_assoc_change& change);

View File

@ -25,7 +25,6 @@
#include "rtc_base/logging.h"
#include "rtc_base/thread.h"
#include "test/gtest.h"
#include "usrsctplib/usrsctp.h"
namespace {
static const int kDefaultTimeout = 10000; // 10 seconds.
@ -239,73 +238,6 @@ class SctpTransportTest : public ::testing::Test, public sigslot::has_slots<> {
void OnChan2ReadyToSend() { ++transport2_ready_to_send_count_; }
};
TEST_F(SctpTransportTest, MessageInterleavedWithNotification) {
FakeDtlsTransport fake_dtls1("fake dtls 1", 0);
FakeDtlsTransport fake_dtls2("fake dtls 2", 0);
SctpFakeDataReceiver recv1;
SctpFakeDataReceiver recv2;
std::unique_ptr<SctpTransport> transport1(
CreateTransport(&fake_dtls1, &recv1));
std::unique_ptr<SctpTransport> transport2(
CreateTransport(&fake_dtls2, &recv2));
// Add a stream.
transport1->OpenStream(1);
transport2->OpenStream(1);
// Start SCTP transports.
transport1->Start(kSctpDefaultPort, kSctpDefaultPort, kSctpSendBufferSize);
transport2->Start(kSctpDefaultPort, kSctpDefaultPort, kSctpSendBufferSize);
// Connect the two fake DTLS transports.
fake_dtls1.SetDestination(&fake_dtls2, false);
// Ensure the SCTP association has been established
// Note: I'd rather watch for an assoc established state here but couldn't
// find any exposed...
SendDataResult result;
ASSERT_TRUE(SendData(transport2.get(), 1, "meow", &result));
EXPECT_TRUE_WAIT(ReceivedData(&recv1, 1, "meow"), kDefaultTimeout);
// Detach the DTLS transport to ensure only we will inject packets from here
// on.
transport1->SetDtlsTransport(nullptr);
// Prepare chunk buffer and metadata
auto chunk = rtc::CopyOnWriteBuffer(32);
struct sctp_rcvinfo meta = {0};
meta.rcv_sid = 1;
meta.rcv_ssn = 1337;
meta.rcv_ppid = rtc::HostToNetwork32(51); // text (complete)
// Inject chunk 1/2.
meta.rcv_tsn = 42;
meta.rcv_cumtsn = 42;
chunk.SetData("meow?", 5);
EXPECT_EQ(1, transport1->InjectDataOrNotificationFromSctpForTesting(
chunk.data(), chunk.size(), meta, 0));
// Inject a notification in between chunks.
union sctp_notification notification;
memset(&notification, 0, sizeof(notification));
// Type chosen since it's not handled apart from being logged
notification.sn_header.sn_type = SCTP_PEER_ADDR_CHANGE;
notification.sn_header.sn_flags = 0;
notification.sn_header.sn_length = sizeof(notification);
EXPECT_EQ(1, transport1->InjectDataOrNotificationFromSctpForTesting(
&notification, sizeof(notification), {0}, MSG_NOTIFICATION));
// Inject chunk 2/2
meta.rcv_tsn = 42;
meta.rcv_cumtsn = 43;
chunk.SetData(" rawr!", 6);
EXPECT_EQ(1, transport1->InjectDataOrNotificationFromSctpForTesting(
chunk.data(), chunk.size(), meta, MSG_EOR));
// Expect the message to contain both chunks.
EXPECT_TRUE_WAIT(ReceivedData(&recv1, 1, "meow? rawr!"), kDefaultTimeout);
}
// Test that data can be sent end-to-end when an SCTP transport starts with one
// transport (which is unwritable), and then switches to another transport. A
// common scenario due to how BUNDLE works.