Skip to content

Commit 2a22c94

Browse files
authored
[fix] h2 double complete (#558)
1 parent 0409bc1 commit 2a22c94

4 files changed

Lines changed: 68 additions & 0 deletions

File tree

include/aws/http/private/h2_stream.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,9 @@ struct aws_h2_stream {
118118
* asleep. When stream needs to be awaken, moving the stream back to the outgoing_streams_list and set this bool
119119
* to false */
120120
bool waiting_for_writes;
121+
/* Set when s_stream_complete has been called. Prevents double-completion
122+
* (e.g. GOAWAY completes stream, then pending cancel cross-thread task fires) */
123+
bool is_complete;
121124
} thread_data;
122125

123126
/* Any thread may touch this data, but the lock must be held (unless it's an atomic) */

source/h2_connection.c

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1868,6 +1868,13 @@ static void s_handler_installed(struct aws_channel_handler *handler, struct aws_
18681868
static void s_stream_complete(struct aws_h2_connection *connection, struct aws_h2_stream *stream, int error_code) {
18691869
AWS_PRECONDITION(aws_channel_thread_is_callers_thread(connection->base.channel_slot->channel));
18701870

1871+
/* Guard against double-completion (e.g. GOAWAY completes stream, then pending cancel task fires) */
1872+
if (stream->thread_data.is_complete) {
1873+
AWS_H2_STREAM_LOG(DEBUG, stream, "stream already completed, ignoring.");
1874+
return;
1875+
}
1876+
stream->thread_data.is_complete = true;
1877+
18711878
/* Nice logging */
18721879
if (error_code) {
18731880
AWS_H2_STREAM_LOGF(

tests/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -712,6 +712,7 @@ add_net_test_case(h2_sm_mock_complete_stream)
712712
add_net_test_case(h2_sm_mock_ideal_num_streams)
713713
add_net_test_case(h2_sm_mock_large_ideal_num_streams)
714714
add_net_test_case(h2_sm_mock_goaway)
715+
add_net_test_case(h2_sm_mock_cancel_after_goaway_no_double_complete)
715716
add_net_test_case(h2_sm_connection_ping)
716717
add_net_test_case(h2_sm_with_flow_control_err)
717718
add_net_test_case(h2_sm_with_initial_settings)

tests/test_stream_manager.c

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1152,6 +1152,63 @@ TEST_CASE(h2_sm_mock_goaway) {
11521152
return s_tester_clean_up();
11531153
}
11541154

1155+
/* Test that cancelling a stream after GOAWAY does not crash from double stream completion.
1156+
* Reproduces P428540711: cancel() schedules cross-thread task, but s_finish_shutdown already
1157+
* completed the stream. The cross-thread task then invokes on_complete a second time. */
1158+
TEST_CASE(h2_sm_mock_cancel_after_goaway_no_double_complete) {
1159+
(void)ctx;
1160+
struct sm_tester_options options = {
1161+
.max_connections = 1,
1162+
.max_concurrent_streams_per_connection = 1,
1163+
.alloc = allocator,
1164+
};
1165+
ASSERT_SUCCESS(s_tester_init(&options));
1166+
s_override_cm_connect_function(s_aws_http_connection_manager_create_connection_sync_mock);
1167+
1168+
/* Acquire 1 stream */
1169+
ASSERT_SUCCESS(s_sm_stream_acquiring(1));
1170+
ASSERT_SUCCESS(s_wait_on_fake_connection_count(1));
1171+
s_drain_all_fake_connection_testing_channel();
1172+
ASSERT_SUCCESS(s_wait_on_streams_acquired_count(1));
1173+
ASSERT_INT_EQUALS(0, s_tester.acquiring_stream_errors);
1174+
1175+
struct sm_fake_connection *fake_connection = s_get_fake_connection(0);
1176+
ASSERT_SUCCESS(h2_fake_peer_send_connection_preface_default_settings(&fake_connection->peer));
1177+
testing_channel_drain_queued_tasks(&fake_connection->testing_channel);
1178+
1179+
/* Get the stream handle */
1180+
struct aws_http_stream *stream = NULL;
1181+
aws_array_list_front(&s_tester.streams, &stream);
1182+
ASSERT_NOT_NULL(stream);
1183+
1184+
/* Send GOAWAY with last_stream_id=0, meaning our stream (id=1) is rejected */
1185+
struct aws_byte_cursor debug_info;
1186+
AWS_ZERO_STRUCT(debug_info);
1187+
struct aws_h2_frame *goaway_frame =
1188+
aws_h2_frame_new_goaway(allocator, 0 /*last_stream_id*/, AWS_HTTP2_ERR_INTERNAL_ERROR, debug_info);
1189+
ASSERT_SUCCESS(h2_fake_peer_send_frame(&fake_connection->peer, goaway_frame));
1190+
1191+
/* Cancel the stream BEFORE draining tasks.
1192+
* This schedules the cross-thread work task (reset_called=true).
1193+
* When we drain, both the GOAWAY processing (which completes the stream)
1194+
* and the cancel cross-thread task will run, causing double on_complete. */
1195+
aws_http_stream_cancel_default_error(stream);
1196+
1197+
/* User releases their ref on the stream (simulates real usage: cancel then release).
1198+
* The stream must stay alive until the cross-thread task completes. */
1199+
aws_http_stream_release(stream);
1200+
aws_array_list_clear(&s_tester.streams);
1201+
1202+
/* Drain tasks - this should NOT crash from double stream completion or use-after-free */
1203+
testing_channel_drain_queued_tasks(&fake_connection->testing_channel);
1204+
1205+
/* Stream should be completed with error at least once */
1206+
ASSERT_TRUE(s_tester.stream_completed_count >= 1);
1207+
ASSERT_TRUE(s_tester.stream_complete_errors >= 1);
1208+
1209+
return s_tester_clean_up();
1210+
}
1211+
11551212
/* Test that PING works as expected. */
11561213
TEST_CASE(h2_sm_connection_ping) {
11571214
(void)ctx;

0 commit comments

Comments
 (0)