Ensure some QuicAlarms are properly cancelled to prevent use-after-free issues when alarm fires.
Also added error messages in QuicAlarm to detect similar issues in production.
Protected by FLAGS_quic_restart_flag_quic_alarm_add_permanent_cancel.
PiperOrigin-RevId: 388316558
diff --git a/quic/core/quic_alarm.cc b/quic/core/quic_alarm.cc
index bfef316..75f7dc5 100644
--- a/quic/core/quic_alarm.cc
+++ b/quic/core/quic_alarm.cc
@@ -4,30 +4,70 @@
#include "quic/core/quic_alarm.h"
+#include "quic/platform/api/quic_bug_tracker.h"
+#include "quic/platform/api/quic_flags.h"
+#include "quic/platform/api/quic_stack_trace.h"
+
namespace quic {
QuicAlarm::QuicAlarm(QuicArenaScopedPtr<Delegate> delegate)
: delegate_(std::move(delegate)), deadline_(QuicTime::Zero()) {}
-QuicAlarm::~QuicAlarm() {}
+QuicAlarm::~QuicAlarm() {
+ if (IsSet()) {
+ QUIC_LOG_EVERY_N_SEC(ERROR, 10 * 60)
+ << "QuicAlarm not cancelled at destruction. This message is rate "
+ "limited to once every 10 minutes. "
+ << QuicStackTrace();
+ }
+}
void QuicAlarm::Set(QuicTime new_deadline) {
QUICHE_DCHECK(!IsSet());
QUICHE_DCHECK(new_deadline.IsInitialized());
+
+ if (IsPermanentlyCancelled()) {
+ QUIC_BUG(quic_alarm_illegal_set)
+ << "Set called after alarm is permanently cancelled. new_deadline:"
+ << new_deadline;
+ return;
+ }
+
deadline_ = new_deadline;
SetImpl();
}
-void QuicAlarm::Cancel() {
- if (!IsSet()) {
- // Don't try to cancel an alarm that hasn't been set.
+void QuicAlarm::CancelInternal(bool permanent) {
+ if (!GetQuicRestartFlag(quic_alarm_add_permanent_cancel)) {
+ if (!IsSet()) {
+ // Don't try to cancel an alarm that hasn't been set.
+ return;
+ }
+ deadline_ = QuicTime::Zero();
+ CancelImpl();
return;
}
- deadline_ = QuicTime::Zero();
- CancelImpl();
+
+ if (IsSet()) {
+ deadline_ = QuicTime::Zero();
+ CancelImpl();
+ }
+
+ if (permanent) {
+ delegate_.reset();
+ }
}
+bool QuicAlarm::IsPermanentlyCancelled() const { return delegate_ == nullptr; }
+
void QuicAlarm::Update(QuicTime new_deadline, QuicTime::Delta granularity) {
+ if (IsPermanentlyCancelled()) {
+ QUIC_BUG(quic_alarm_illegal_update)
+ << "Update called after alarm is permanently cancelled. new_deadline:"
+ << new_deadline << ", granularity:" << granularity;
+ return;
+ }
+
if (!new_deadline.IsInitialized()) {
Cancel();
return;
@@ -55,7 +95,9 @@
}
deadline_ = QuicTime::Zero();
- delegate_->OnAlarm();
+ if (!IsPermanentlyCancelled()) {
+ delegate_->OnAlarm();
+ }
}
void QuicAlarm::UpdateImpl() {
diff --git a/quic/core/quic_alarm.h b/quic/core/quic_alarm.h
index 527d2e3..5080fd4 100644
--- a/quic/core/quic_alarm.h
+++ b/quic/core/quic_alarm.h
@@ -36,11 +36,18 @@
// then Set().
void Set(QuicTime new_deadline);
- // Cancels the alarm. May be called repeatedly. Does not
- // guarantee that the underlying scheduling system will remove
- // the alarm's associated task, but guarantees that the
- // delegates OnAlarm method will not be called.
- void Cancel();
+ // Both PermanentCancel() and Cancel() can cancel the alarm. If permanent,
+ // future calls to Set() and Update() will become no-op except emitting an
+ // error log.
+ //
+ // Both may be called repeatedly. Does not guarantee that the underlying
+ // scheduling system will remove the alarm's associated task, but guarantees
+ // that the delegates OnAlarm method will not be called.
+ void PermanentCancel() { CancelInternal(true); }
+ void Cancel() { CancelInternal(false); }
+
+ // Return true if PermanentCancel() has been called.
+ bool IsPermanentlyCancelled() const;
// Cancels and sets the alarm if the |deadline| is farther from the current
// deadline than |granularity|, and otherwise does nothing. If |deadline| is
@@ -77,6 +84,8 @@
void Fire();
private:
+ void CancelInternal(bool permanent);
+
QuicArenaScopedPtr<Delegate> delegate_;
QuicTime deadline_;
};
diff --git a/quic/core/quic_alarm_test.cc b/quic/core/quic_alarm_test.cc
index 8f7296c..ab07002 100644
--- a/quic/core/quic_alarm_test.cc
+++ b/quic/core/quic_alarm_test.cc
@@ -4,6 +4,7 @@
#include "quic/core/quic_alarm.h"
+#include "quic/platform/api/quic_expect_bug.h"
#include "quic/platform/api/quic_test.h"
using testing::Invoke;
@@ -111,6 +112,48 @@
EXPECT_EQ(QuicTime::Zero(), alarm_.deadline());
}
+TEST_F(QuicAlarmTest, PermanentCancel) {
+ QuicTime deadline = QuicTime::Zero() + QuicTime::Delta::FromSeconds(7);
+ alarm_.Set(deadline);
+ alarm_.PermanentCancel();
+ EXPECT_FALSE(alarm_.IsSet());
+ EXPECT_FALSE(alarm_.scheduled());
+ EXPECT_EQ(QuicTime::Zero(), alarm_.deadline());
+
+ if (!GetQuicRestartFlag(quic_alarm_add_permanent_cancel)) {
+ alarm_.Set(deadline);
+ // When flag is false, PermanentCancel should work like a normal Cancel.
+ EXPECT_TRUE(alarm_.IsSet());
+ EXPECT_TRUE(alarm_.scheduled());
+ EXPECT_EQ(deadline, alarm_.deadline());
+ EXPECT_FALSE(alarm_.IsPermanentlyCancelled());
+ alarm_.PermanentCancel();
+ } else {
+ EXPECT_QUIC_BUG(alarm_.Set(deadline),
+ "Set called after alarm is permanently cancelled");
+ EXPECT_TRUE(alarm_.IsPermanentlyCancelled());
+ }
+ EXPECT_FALSE(alarm_.IsSet());
+ EXPECT_FALSE(alarm_.scheduled());
+ EXPECT_EQ(QuicTime::Zero(), alarm_.deadline());
+
+ if (!GetQuicRestartFlag(quic_alarm_add_permanent_cancel)) {
+ alarm_.Update(deadline, QuicTime::Delta::Zero());
+ EXPECT_TRUE(alarm_.IsSet());
+ EXPECT_TRUE(alarm_.scheduled());
+ EXPECT_EQ(deadline, alarm_.deadline());
+ EXPECT_FALSE(alarm_.IsPermanentlyCancelled());
+ alarm_.PermanentCancel();
+ } else {
+ EXPECT_QUIC_BUG(alarm_.Update(deadline, QuicTime::Delta::Zero()),
+ "Update called after alarm is permanently cancelled");
+ EXPECT_TRUE(alarm_.IsPermanentlyCancelled());
+ }
+ EXPECT_FALSE(alarm_.IsSet());
+ EXPECT_FALSE(alarm_.scheduled());
+ EXPECT_EQ(QuicTime::Zero(), alarm_.deadline());
+}
+
TEST_F(QuicAlarmTest, Update) {
QuicTime deadline = QuicTime::Zero() + QuicTime::Delta::FromSeconds(7);
alarm_.Set(deadline);
diff --git a/quic/core/quic_buffered_packet_store.cc b/quic/core/quic_buffered_packet_store.cc
index 078e222..9312ea5 100644
--- a/quic/core/quic_buffered_packet_store.cc
+++ b/quic/core/quic_buffered_packet_store.cc
@@ -76,7 +76,12 @@
expiration_alarm_(
alarm_factory->CreateAlarm(new ConnectionExpireAlarm(this))) {}
-QuicBufferedPacketStore::~QuicBufferedPacketStore() {}
+QuicBufferedPacketStore::~QuicBufferedPacketStore() {
+ if (GetQuicRestartFlag(quic_alarm_add_permanent_cancel) &&
+ expiration_alarm_ != nullptr) {
+ expiration_alarm_->PermanentCancel();
+ }
+}
EnqueuePacketResult QuicBufferedPacketStore::EnqueuePacket(
QuicConnectionId connection_id,
diff --git a/quic/core/quic_connection.cc b/quic/core/quic_connection.cc
index d3b3e82..bbdaf9b 100644
--- a/quic/core/quic_connection.cc
+++ b/quic/core/quic_connection.cc
@@ -4251,7 +4251,7 @@
blackhole_detector_.IsDetectionInProgress()) {
// Stop detection in quiescence.
QUICHE_DCHECK_EQ(QuicSentPacketManager::LOSS_MODE, retransmission_mode);
- blackhole_detector_.StopDetection();
+ blackhole_detector_.StopDetection(/*permanent=*/false);
}
WriteIfNotBlocked();
@@ -4766,15 +4766,15 @@
void QuicConnection::CancelAllAlarms() {
QUIC_DVLOG(1) << "Cancelling all QuicConnection alarms.";
- ack_alarm_->Cancel();
- ping_alarm_->Cancel();
- retransmission_alarm_->Cancel();
- send_alarm_->Cancel();
- mtu_discovery_alarm_->Cancel();
- process_undecryptable_packets_alarm_->Cancel();
- discard_previous_one_rtt_keys_alarm_->Cancel();
- discard_zero_rtt_decryption_keys_alarm_->Cancel();
- blackhole_detector_.StopDetection();
+ ack_alarm_->PermanentCancel();
+ ping_alarm_->PermanentCancel();
+ retransmission_alarm_->PermanentCancel();
+ send_alarm_->PermanentCancel();
+ mtu_discovery_alarm_->PermanentCancel();
+ process_undecryptable_packets_alarm_->PermanentCancel();
+ discard_previous_one_rtt_keys_alarm_->PermanentCancel();
+ discard_zero_rtt_decryption_keys_alarm_->PermanentCancel();
+ blackhole_detector_.StopDetection(/*permanent=*/true);
idle_network_detector_.StopDetection();
}
@@ -4808,6 +4808,9 @@
}
void QuicConnection::SetPingAlarm() {
+ if (!connected_) {
+ return;
+ }
if (perspective_ == Perspective::IS_SERVER &&
initial_retransmittable_on_wire_timeout_.IsInfinite()) {
// The PING alarm exists to support two features:
@@ -5806,7 +5809,7 @@
// In case no new packets get acknowledged, it is possible packets are
// detected lost because of time based loss detection. Cancel blackhole
// detection if there is no packets in flight.
- blackhole_detector_.StopDetection();
+ blackhole_detector_.StopDetection(/*permanent=*/false);
}
if (send_stop_waiting) {
@@ -6186,6 +6189,9 @@
}
void QuicConnection::OnForwardProgressMade() {
+ if (GetQuicRestartFlag(quic_alarm_add_permanent_cancel) && !connected_) {
+ return;
+ }
if (is_path_degrading_) {
visitor_->OnForwardProgressMadeAfterPathDegrading();
is_path_degrading_ = false;
@@ -6197,7 +6203,7 @@
GetPathMtuReductionDeadline());
} else {
// Stop detections in quiecense.
- blackhole_detector_.StopDetection();
+ blackhole_detector_.StopDetection(/*permanent=*/false);
}
QUIC_BUG_IF(quic_bug_12714_35,
default_enable_5rto_blackhole_detection_ &&
@@ -7159,7 +7165,7 @@
// re-arm it.
SetRetransmissionAlarm();
// Stop detections in quiecense.
- blackhole_detector_.StopDetection();
+ blackhole_detector_.StopDetection(/*permanent=*/false);
return old_send_algorithm;
}
diff --git a/quic/core/quic_dispatcher.cc b/quic/core/quic_dispatcher.cc
index 6518754..11f4bcd 100644
--- a/quic/core/quic_dispatcher.cc
+++ b/quic/core/quic_dispatcher.cc
@@ -340,9 +340,14 @@
<< "Trying to create dispatcher without any supported versions";
QUIC_DLOG(INFO) << "Created QuicDispatcher with versions: "
<< ParsedQuicVersionVectorToString(GetSupportedVersions());
+ QUIC_RESTART_FLAG_COUNT(quic_alarm_add_permanent_cancel);
}
QuicDispatcher::~QuicDispatcher() {
+ if (GetQuicRestartFlag(quic_alarm_add_permanent_cancel) &&
+ delete_sessions_alarm_ != nullptr) {
+ delete_sessions_alarm_->PermanentCancel();
+ }
reference_counted_session_map_.clear();
closed_ref_counted_session_list_.clear();
if (support_multiple_cid_per_connection_) {
diff --git a/quic/core/quic_flags_list.h b/quic/core/quic_flags_list.h
index 84f50ee..2089704 100644
--- a/quic/core/quic_flags_list.h
+++ b/quic/core/quic_flags_list.h
@@ -33,6 +33,8 @@
QUIC_FLAG(FLAGS_quic_reloadable_flag_quic_can_send_ack_frequency, true)
// If true, add missing MaybeUpdateAckTimeout for ack-eliciting frames.
QUIC_FLAG(FLAGS_quic_reloadable_flag_quic_add_missing_update_ack_timeout, true)
+// If true, allow QuicAlarm to be permanently cancelled.
+QUIC_FLAG(FLAGS_quic_restart_flag_quic_alarm_add_permanent_cancel, true)
// If true, allow client to enable BBRv2 on server via connection option \'B2ON\'.
QUIC_FLAG(FLAGS_quic_reloadable_flag_quic_allow_client_enabled_bbr_v2, false)
// If true, allow ticket open to be ignored in TlsServerHandshaker. Also fixes TlsServerHandshaker::ResumptionAttempted when handshake hints is used.
diff --git a/quic/core/quic_idle_network_detector.cc b/quic/core/quic_idle_network_detector.cc
index ba81601..ac6799e 100644
--- a/quic/core/quic_idle_network_detector.cc
+++ b/quic/core/quic_idle_network_detector.cc
@@ -70,7 +70,7 @@
}
void QuicIdleNetworkDetector::StopDetection() {
- alarm_->Cancel();
+ alarm_->PermanentCancel();
handshake_timeout_ = QuicTime::Delta::Infinite();
idle_network_timeout_ = QuicTime::Delta::Infinite();
stopped_ = true;
diff --git a/quic/core/quic_network_blackhole_detector.cc b/quic/core/quic_network_blackhole_detector.cc
index 5281ffb..4dca508 100644
--- a/quic/core/quic_network_blackhole_detector.cc
+++ b/quic/core/quic_network_blackhole_detector.cc
@@ -64,8 +64,12 @@
UpdateAlarm();
}
-void QuicNetworkBlackholeDetector::StopDetection() {
- alarm_->Cancel();
+void QuicNetworkBlackholeDetector::StopDetection(bool permanent) {
+ if (permanent) {
+ alarm_->PermanentCancel();
+ } else {
+ alarm_->Cancel();
+ }
path_degrading_deadline_ = QuicTime::Zero();
blackhole_deadline_ = QuicTime::Zero();
path_mtu_reduction_deadline_ = QuicTime::Zero();
@@ -108,6 +112,12 @@
}
void QuicNetworkBlackholeDetector::UpdateAlarm() const {
+ // If called after OnBlackholeDetected(), the alarm may have been permanently
+ // cancelled and is not safe to be armed again.
+ if (alarm_->IsPermanentlyCancelled()) {
+ return;
+ }
+
QuicTime next_deadline = GetEarliestDeadline();
QUIC_DVLOG(1) << "Updating alarm. next_deadline:" << next_deadline
diff --git a/quic/core/quic_network_blackhole_detector.h b/quic/core/quic_network_blackhole_detector.h
index 7010eb5..82753f9 100644
--- a/quic/core/quic_network_blackhole_detector.h
+++ b/quic/core/quic_network_blackhole_detector.h
@@ -44,8 +44,9 @@
QuicConnectionArena* arena,
QuicAlarmFactory* alarm_factory);
- // Called to stop all detections.
- void StopDetection();
+ // Called to stop all detections. If |permanent|, the alarm will be cancelled
+ // permanently and future calls to RestartDetection will be no-op.
+ void StopDetection(bool permanent);
// Called to restart path degrading, path mtu reduction and blackhole
// detections. Please note, if |blackhole_deadline| is set, it must be the
diff --git a/quic/core/quic_network_blackhole_detector_test.cc b/quic/core/quic_network_blackhole_detector_test.cc
index 3bc6ad7..66bebfd 100644
--- a/quic/core/quic_network_blackhole_detector_test.cc
+++ b/quic/core/quic_network_blackhole_detector_test.cc
@@ -106,7 +106,7 @@
RestartDetection();
EXPECT_EQ(clock_.Now() + path_degrading_delay_, alarm_->deadline());
- detector_.StopDetection();
+ detector_.StopDetection(/*permanent=*/false);
EXPECT_FALSE(detector_.IsDetectionInProgress());
}
diff --git a/quic/core/quic_session.cc b/quic/core/quic_session.cc
index df32cc5..aef3fc2 100644
--- a/quic/core/quic_session.cc
+++ b/quic/core/quic_session.cc
@@ -166,7 +166,12 @@
GetMutableCryptoStream()->id());
}
-QuicSession::~QuicSession() {}
+QuicSession::~QuicSession() {
+ if (GetQuicRestartFlag(quic_alarm_add_permanent_cancel) &&
+ closed_streams_clean_up_alarm_ != nullptr) {
+ closed_streams_clean_up_alarm_->PermanentCancel();
+ }
+}
void QuicSession::PendingStreamOnStreamFrame(const QuicStreamFrame& frame) {
QUICHE_DCHECK(VersionUsesHttp3(transport_version()));
diff --git a/quic/test_tools/packet_dropping_test_writer.cc b/quic/test_tools/packet_dropping_test_writer.cc
index 35cc978..76dd778 100644
--- a/quic/test_tools/packet_dropping_test_writer.cc
+++ b/quic/test_tools/packet_dropping_test_writer.cc
@@ -68,7 +68,14 @@
simple_random_.set_seed(seed);
}
-PacketDroppingTestWriter::~PacketDroppingTestWriter() = default;
+PacketDroppingTestWriter::~PacketDroppingTestWriter() {
+ if (write_unblocked_alarm_ != nullptr) {
+ write_unblocked_alarm_->PermanentCancel();
+ }
+ if (delay_alarm_ != nullptr) {
+ delay_alarm_->PermanentCancel();
+ }
+}
void PacketDroppingTestWriter::Initialize(
QuicConnectionHelperInterface* helper,
diff --git a/quic/test_tools/simulator/queue.cc b/quic/test_tools/simulator/queue.cc
index c286bf6..c7e23cd 100644
--- a/quic/test_tools/simulator/queue.cc
+++ b/quic/test_tools/simulator/queue.cc
@@ -26,7 +26,7 @@
new AggregationAlarmDelegate(this)));
}
-Queue::~Queue() {}
+Queue::~Queue() { aggregation_timeout_alarm_->PermanentCancel(); }
void Queue::set_tx_port(ConstrainedPortInterface* port) {
tx_port_ = port;