@@ -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. */
11561213TEST_CASE (h2_sm_connection_ping ) {
11571214 (void )ctx ;
0 commit comments