If StreamStart is called after the connection has received shutdown-complete, StreamStart fails with 0x80004004, yet the application still receives callbacks that were stated to never be delivered.
Documentation explicitly indicates no callbacks should be delivered for such streams,
#include <windows.h>
#include <wincrypt.h>
#include <ncrypt.h>
#include <atomic>
#include <chrono>
#include <cstdarg>
#include <cstdio>
#include <mutex>
#include <string>
#include <thread>
#include <vector>
#include "msquic.h"
namespace {
constexpr char kAlpnValue[] = "stream-close-repro";
constexpr auto kWaitTimeout = std::chrono::seconds(15);
std::mutex gLogLock;
void Log(const char* format, ...)
{
std::lock_guard<std::mutex> lock(gLogLock);
std::printf("[thread %lu] ", GetCurrentThreadId());
va_list arguments;
va_start(arguments, format);
std::vprintf(format, arguments);
va_end(arguments);
std::printf("\n");
std::fflush(stdout);
}
const char* StreamEventName(QUIC_STREAM_EVENT_TYPE type)
{
switch (type) {
case QUIC_STREAM_EVENT_START_COMPLETE: return "START_COMPLETE";
case QUIC_STREAM_EVENT_RECEIVE: return "RECEIVE";
case QUIC_STREAM_EVENT_SEND_COMPLETE: return "SEND_COMPLETE";
case QUIC_STREAM_EVENT_PEER_SEND_SHUTDOWN: return "PEER_SEND_SHUTDOWN";
case QUIC_STREAM_EVENT_PEER_SEND_ABORTED: return "PEER_SEND_ABORTED";
case QUIC_STREAM_EVENT_PEER_RECEIVE_ABORTED: return "PEER_RECEIVE_ABORTED";
case QUIC_STREAM_EVENT_SEND_SHUTDOWN_COMPLETE: return "SEND_SHUTDOWN_COMPLETE";
case QUIC_STREAM_EVENT_SHUTDOWN_COMPLETE: return "SHUTDOWN_COMPLETE";
case QUIC_STREAM_EVENT_IDEAL_SEND_BUFFER_SIZE: return "IDEAL_SEND_BUFFER_SIZE";
case QUIC_STREAM_EVENT_PEER_ACCEPTED: return "PEER_ACCEPTED";
case QUIC_STREAM_EVENT_CANCEL_ON_LOSS: return "CANCEL_ON_LOSS";
default: return "UNKNOWN";
}
}
class RuntimeCertificate final {
public:
RuntimeCertificate() = default;
RuntimeCertificate(const RuntimeCertificate&) = delete;
RuntimeCertificate& operator=(const RuntimeCertificate&) = delete;
~RuntimeCertificate()
{
Reset();
}
bool Create()
{
wchar_t keyName[96]{};
_snwprintf_s(
keyName,
_countof(keyName),
_TRUNCATE,
L"MsQuicStreamCloseRepro-%lu-%llu",
GetCurrentProcessId(),
GetTickCount64());
KeyName = keyName;
SECURITY_STATUS securityStatus =
NCryptOpenStorageProvider(&Provider, MS_KEY_STORAGE_PROVIDER, 0);
if (securityStatus != ERROR_SUCCESS) {
Log("NCryptOpenStorageProvider failed: 0x%08X", static_cast<unsigned>(securityStatus));
return false;
}
securityStatus =
NCryptCreatePersistedKey(
Provider,
&Key,
NCRYPT_RSA_ALGORITHM,
KeyName.c_str(),
0,
0);
if (securityStatus != ERROR_SUCCESS) {
Log("NCryptCreatePersistedKey failed: 0x%08X", static_cast<unsigned>(securityStatus));
return false;
}
DWORD keyLength = 2048;
securityStatus =
NCryptSetProperty(
Key,
NCRYPT_LENGTH_PROPERTY,
reinterpret_cast<PBYTE>(&keyLength),
sizeof(keyLength),
0);
if (securityStatus != ERROR_SUCCESS) {
Log("NCryptSetProperty(length) failed: 0x%08X", static_cast<unsigned>(securityStatus));
return false;
}
DWORD keyUsage = NCRYPT_ALLOW_SIGNING_FLAG;
securityStatus =
NCryptSetProperty(
Key,
NCRYPT_KEY_USAGE_PROPERTY,
reinterpret_cast<PBYTE>(&keyUsage),
sizeof(keyUsage),
0);
if (securityStatus != ERROR_SUCCESS) {
Log("NCryptSetProperty(usage) failed: 0x%08X", static_cast<unsigned>(securityStatus));
return false;
}
DWORD exportPolicy = NCRYPT_ALLOW_PLAINTEXT_EXPORT_FLAG;
securityStatus =
NCryptSetProperty(
Key,
NCRYPT_EXPORT_POLICY_PROPERTY,
reinterpret_cast<PBYTE>(&exportPolicy),
sizeof(exportPolicy),
0);
if (securityStatus != ERROR_SUCCESS) {
Log("NCryptSetProperty(export policy) failed: 0x%08X", static_cast<unsigned>(securityStatus));
return false;
}
securityStatus = NCryptFinalizeKey(Key, 0);
if (securityStatus != ERROR_SUCCESS) {
Log("NCryptFinalizeKey failed: 0x%08X", static_cast<unsigned>(securityStatus));
return false;
}
DWORD subjectLength = 0;
if (!CertStrToNameW(
X509_ASN_ENCODING,
L"CN=localhost",
CERT_X500_NAME_STR,
nullptr,
nullptr,
&subjectLength,
nullptr)) {
Log("CertStrToNameW(size) failed: %lu", GetLastError());
return false;
}
std::vector<BYTE> subject(subjectLength);
if (!CertStrToNameW(
X509_ASN_ENCODING,
L"CN=localhost",
CERT_X500_NAME_STR,
nullptr,
subject.data(),
&subjectLength,
nullptr)) {
Log("CertStrToNameW(data) failed: %lu", GetLastError());
return false;
}
CERT_NAME_BLOB subjectBlob{};
subjectBlob.cbData = subjectLength;
subjectBlob.pbData = subject.data();
CRYPT_KEY_PROV_INFO keyProviderInfo{};
keyProviderInfo.pwszContainerName = KeyName.data();
keyProviderInfo.pwszProvName = const_cast<LPWSTR>(MS_KEY_STORAGE_PROVIDER);
keyProviderInfo.dwKeySpec = AT_KEYEXCHANGE;
CRYPT_ALGORITHM_IDENTIFIER signatureAlgorithm{};
signatureAlgorithm.pszObjId = const_cast<LPSTR>(szOID_RSA_SHA256RSA);
Context =
CertCreateSelfSignCertificate(
Key,
&subjectBlob,
0,
&keyProviderInfo,
&signatureAlgorithm,
nullptr,
nullptr,
nullptr);
if (Context == nullptr) {
Log("CertCreateSelfSignCertificate failed: %lu", GetLastError());
return false;
}
Log("created temporary self-signed server certificate (no files or store install)");
return true;
}
PCCERT_CONTEXT Get() const
{
return Context;
}
private:
void Reset()
{
if (Context != nullptr) {
CertFreeCertificateContext(Context);
Context = nullptr;
}
if (Key != 0) {
NCryptDeleteKey(Key, NCRYPT_SILENT_FLAG);
Key = 0;
}
if (Provider != 0) {
NCryptFreeObject(Provider);
Provider = 0;
}
}
NCRYPT_PROV_HANDLE Provider{0};
NCRYPT_KEY_HANDLE Key{0};
PCCERT_CONTEXT Context{nullptr};
std::wstring KeyName;
};
struct ReproState final {
const QUIC_API_TABLE* Api{nullptr};
HQUIC ServerConfiguration{nullptr};
std::atomic<HQUIC> ServerConnection{nullptr};
std::atomic<bool> ShutdownRequested{false};
std::atomic<bool> ClientConnected{false};
std::atomic<bool> ClientShutdownComplete{false};
std::atomic<bool> ServerShutdownComplete{false};
std::atomic<bool> InsideStreamClose{false};
std::atomic<uint32_t> StreamCallbackCount{0};
std::atomic<uint32_t> CallbacksInsideStreamClose{0};
};
QUIC_STATUS QUIC_API ServerStreamCallback(
HQUIC stream,
void* context,
QUIC_STREAM_EVENT* event)
{
auto* state = static_cast<ReproState*>(context);
Log("server stream %p: %s", stream, StreamEventName(event->Type));
if (event->Type == QUIC_STREAM_EVENT_SHUTDOWN_COMPLETE &&
!event->SHUTDOWN_COMPLETE.AppCloseInProgress) {
state->Api->StreamClose(stream);
}
return QUIC_STATUS_SUCCESS;
}
QUIC_STATUS QUIC_API ServerConnectionCallback(
HQUIC connection,
void* context,
QUIC_CONNECTION_EVENT* event)
{
auto* state = static_cast<ReproState*>(context);
switch (event->Type) {
case QUIC_CONNECTION_EVENT_CONNECTED:
Log("server connection %p: CONNECTED", connection);
break;
case QUIC_CONNECTION_EVENT_SHUTDOWN_INITIATED_BY_TRANSPORT:
Log(
"server connection %p: SHUTDOWN_INITIATED_BY_TRANSPORT status=0x%08X",
connection,
static_cast<unsigned>(event->SHUTDOWN_INITIATED_BY_TRANSPORT.Status));
break;
case QUIC_CONNECTION_EVENT_SHUTDOWN_INITIATED_BY_PEER:
Log(
"server connection %p: SHUTDOWN_INITIATED_BY_PEER error=%llu",
connection,
static_cast<unsigned long long>(event->SHUTDOWN_INITIATED_BY_PEER.ErrorCode));
break;
case QUIC_CONNECTION_EVENT_SHUTDOWN_COMPLETE:
Log("server connection %p: SHUTDOWN_COMPLETE", connection);
state->ServerShutdownComplete.store(true, std::memory_order_release);
break;
case QUIC_CONNECTION_EVENT_PEER_STREAM_STARTED:
Log("server connection %p: PEER_STREAM_STARTED %p", connection, event->PEER_STREAM_STARTED.Stream);
state->Api->SetCallbackHandler(
event->PEER_STREAM_STARTED.Stream,
reinterpret_cast<void*>(ServerStreamCallback),
state);
break;
default:
break;
}
return QUIC_STATUS_SUCCESS;
}
QUIC_STATUS QUIC_API ListenerCallback(
HQUIC listener,
void* context,
QUIC_LISTENER_EVENT* event)
{
auto* state = static_cast<ReproState*>(context);
if (event->Type != QUIC_LISTENER_EVENT_NEW_CONNECTION) {
return QUIC_STATUS_NOT_SUPPORTED;
}
HQUIC connection = event->NEW_CONNECTION.Connection;
Log("listener %p: NEW_CONNECTION %p", listener, connection);
state->ServerConnection.store(connection, std::memory_order_release);
state->Api->SetCallbackHandler(
connection,
reinterpret_cast<void*>(ServerConnectionCallback),
state);
return state->Api->ConnectionSetConfiguration(connection, state->ServerConfiguration);
}
QUIC_STATUS QUIC_API ClientStreamCallback(
HQUIC stream,
void* context,
QUIC_STREAM_EVENT* event)
{
auto* state = static_cast<ReproState*>(context);
const bool insideClose = state->InsideStreamClose.load(std::memory_order_acquire);
state->StreamCallbackCount.fetch_add(1, std::memory_order_relaxed);
if (insideClose) {
state->CallbacksInsideStreamClose.fetch_add(1, std::memory_order_relaxed);
}
if (event->Type == QUIC_STREAM_EVENT_START_COMPLETE) {
Log(
"control stream %p callback: %s status=0x%08X inside StreamClose=%s",
stream,
StreamEventName(event->Type),
static_cast<unsigned>(event->START_COMPLETE.Status),
insideClose ? "yes" : "no");
} else if (event->Type == QUIC_STREAM_EVENT_SHUTDOWN_COMPLETE) {
Log(
"control stream %p callback: %s app-close=%u connection-shutdown=%u inside StreamClose=%s",
stream,
StreamEventName(event->Type),
event->SHUTDOWN_COMPLETE.AppCloseInProgress,
event->SHUTDOWN_COMPLETE.ConnectionShutdown,
insideClose ? "yes" : "no");
} else {
Log(
"control stream %p callback: %s inside StreamClose=%s",
stream,
StreamEventName(event->Type),
insideClose ? "yes" : "no");
}
return QUIC_STATUS_SUCCESS;
}
QUIC_STATUS QUIC_API ClientConnectionCallback(
HQUIC connection,
void* context,
QUIC_CONNECTION_EVENT* event)
{
auto* state = static_cast<ReproState*>(context);
switch (event->Type) {
case QUIC_CONNECTION_EVENT_CONNECTED:
Log("client connection %p: CONNECTED", connection);
state->ClientConnected.store(true, std::memory_order_release);
if (!state->ShutdownRequested.exchange(true, std::memory_order_acq_rel)) {
Log("client connection %p: requesting shutdown while control stream is still unstarted", connection);
state->Api->ConnectionShutdown(connection, QUIC_CONNECTION_SHUTDOWN_FLAG_NONE, 0);
}
break;
case QUIC_CONNECTION_EVENT_SHUTDOWN_INITIATED_BY_TRANSPORT:
Log(
"client connection %p: SHUTDOWN_INITIATED_BY_TRANSPORT status=0x%08X",
connection,
static_cast<unsigned>(event->SHUTDOWN_INITIATED_BY_TRANSPORT.Status));
break;
case QUIC_CONNECTION_EVENT_SHUTDOWN_INITIATED_BY_PEER:
Log(
"client connection %p: SHUTDOWN_INITIATED_BY_PEER error=%llu",
connection,
static_cast<unsigned long long>(event->SHUTDOWN_INITIATED_BY_PEER.ErrorCode));
break;
case QUIC_CONNECTION_EVENT_SHUTDOWN_COMPLETE:
Log("client connection %p: SHUTDOWN_COMPLETE; toggling atomic flag", connection);
state->ClientShutdownComplete.store(true, std::memory_order_release);
break;
default:
break;
}
return QUIC_STATUS_SUCCESS;
}
bool YieldUntil(
const char* description,
const std::atomic<bool>& flag)
{
const auto deadline = std::chrono::steady_clock::now() + kWaitTimeout;
uint64_t yieldCount = 0;
while (!flag.load(std::memory_order_acquire)) {
if (std::chrono::steady_clock::now() >= deadline) {
Log("timed out yielding for %s", description);
return false;
}
++yieldCount;
std::this_thread::yield();
}
Log("observed %s after %llu yields", description, static_cast<unsigned long long>(yieldCount));
return true;
}
bool CheckStatus(const char* operation, QUIC_STATUS status)
{
if (QUIC_FAILED(status)) {
Log("%s failed: 0x%08X", operation, static_cast<unsigned>(status));
return false;
}
return true;
}
} // namespace
int main()
{
int result = 1;
const QUIC_API_TABLE* api = nullptr;
HQUIC registration = nullptr;
HQUIC serverConfiguration = nullptr;
HQUIC clientConfiguration = nullptr;
HQUIC listener = nullptr;
HQUIC clientConnection = nullptr;
HQUIC controlStream = nullptr;
RuntimeCertificate certificate;
ReproState state;
do {
if (!CheckStatus("MsQuicOpen2", MsQuicOpen2(&api))) {
break;
}
state.Api = api;
const QUIC_REGISTRATION_CONFIG registrationConfig{
"msquic-stream-close-repro",
QUIC_EXECUTION_PROFILE_LOW_LATENCY};
if (!CheckStatus(
"RegistrationOpen",
api->RegistrationOpen(®istrationConfig, ®istration))) {
break;
}
if (!certificate.Create()) {
break;
}
QUIC_BUFFER alpn{
static_cast<uint32_t>(sizeof(kAlpnValue) - 1),
reinterpret_cast<uint8_t*>(const_cast<char*>(kAlpnValue))};
QUIC_SETTINGS serverSettings{};
serverSettings.IdleTimeoutMs = 5000;
serverSettings.IsSet.IdleTimeoutMs = TRUE;
serverSettings.PeerBidiStreamCount = 1;
serverSettings.IsSet.PeerBidiStreamCount = TRUE;
if (!CheckStatus(
"ConfigurationOpen(server)",
api->ConfigurationOpen(
registration,
&alpn,
1,
&serverSettings,
sizeof(serverSettings),
nullptr,
&serverConfiguration))) {
break;
}
state.ServerConfiguration = serverConfiguration;
QUIC_CREDENTIAL_CONFIG serverCredential{};
serverCredential.Type = QUIC_CREDENTIAL_TYPE_CERTIFICATE_CONTEXT;
serverCredential.CertificateContext =
const_cast<CERT_CONTEXT*>(certificate.Get());
if (!CheckStatus(
"ConfigurationLoadCredential(server)",
api->ConfigurationLoadCredential(serverConfiguration, &serverCredential))) {
break;
}
QUIC_SETTINGS clientSettings{};
clientSettings.IdleTimeoutMs = 5000;
clientSettings.IsSet.IdleTimeoutMs = TRUE;
if (!CheckStatus(
"ConfigurationOpen(client)",
api->ConfigurationOpen(
registration,
&alpn,
1,
&clientSettings,
sizeof(clientSettings),
nullptr,
&clientConfiguration))) {
break;
}
QUIC_CREDENTIAL_CONFIG clientCredential{};
clientCredential.Type = QUIC_CREDENTIAL_TYPE_NONE;
clientCredential.Flags = static_cast<QUIC_CREDENTIAL_FLAGS>(
QUIC_CREDENTIAL_FLAG_CLIENT |
QUIC_CREDENTIAL_FLAG_NO_CERTIFICATE_VALIDATION);
if (!CheckStatus(
"ConfigurationLoadCredential(client/no-validation)",
api->ConfigurationLoadCredential(clientConfiguration, &clientCredential))) {
break;
}
if (!CheckStatus(
"ListenerOpen",
api->ListenerOpen(registration, ListenerCallback, &state, &listener))) {
break;
}
QUIC_ADDR listenAddress{};
QuicAddrSetFamily(&listenAddress, QUIC_ADDRESS_FAMILY_INET);
QuicAddrSetToLoopback(&listenAddress);
QuicAddrSetPort(&listenAddress, 0);
if (!CheckStatus(
"ListenerStart",
api->ListenerStart(listener, &alpn, 1, &listenAddress))) {
break;
}
QUIC_ADDR boundAddress{};
uint32_t boundAddressLength = sizeof(boundAddress);
if (!CheckStatus(
"GetParam(listener local address)",
api->GetParam(
listener,
QUIC_PARAM_LISTENER_LOCAL_ADDRESS,
&boundAddressLength,
&boundAddress))) {
break;
}
const uint16_t port = QuicAddrGetPort(&boundAddress);
Log("local listener started on 127.0.0.1:%u", port);
if (!CheckStatus(
"ConnectionOpen(client)",
api->ConnectionOpen(
registration,
ClientConnectionCallback,
&state,
&clientConnection))) {
break;
}
if (!CheckStatus(
"StreamOpen(control)",
api->StreamOpen(
clientConnection,
QUIC_STREAM_OPEN_FLAG_NONE,
ClientStreamCallback,
&state,
&controlStream))) {
break;
}
Log("control stream %p opened and intentionally left unstarted", controlStream);
if (!CheckStatus(
"ConnectionStart(client)",
api->ConnectionStart(
clientConnection,
clientConfiguration,
QUIC_ADDRESS_FAMILY_INET,
"127.0.0.1",
port))) {
break;
}
Log("yielding until client QUIC_CONNECTION_EVENT_SHUTDOWN_COMPLETE");
if (!YieldUntil("client connection shutdown-complete atomic", state.ClientShutdownComplete)) {
break;
}
Log("calling StreamStart(%p) after connection shutdown completed", controlStream);
const QUIC_STATUS startStatus =
api->StreamStart(controlStream, QUIC_STREAM_START_FLAG_NONE);
Log("StreamStart returned 0x%08X", static_cast<unsigned>(startStatus));
Log("calling StreamClose(%p); control-stream callbacks may be inline", controlStream);
state.InsideStreamClose.store(true, std::memory_order_release);
api->StreamClose(controlStream);
state.InsideStreamClose.store(false, std::memory_order_release);
controlStream = nullptr;
const uint32_t inlineCallbackCount =
state.CallbacksInsideStreamClose.load(std::memory_order_acquire);
Log(
"StreamClose returned; callbacks=%u, callbacks inside StreamClose=%u",
state.StreamCallbackCount.load(std::memory_order_acquire),
inlineCallbackCount);
if (inlineCallbackCount == 0) {
Log("REPRO NOT OBSERVED: StreamClose did not deliver a control-stream callback inline");
result = 2;
break;
}
Log("REPRO OBSERVED: StreamClose synchronously delivered a control-stream callback");
result = 0;
} while (false);
if (controlStream != nullptr && api != nullptr) {
state.InsideStreamClose.store(true, std::memory_order_release);
api->StreamClose(controlStream);
state.InsideStreamClose.store(false, std::memory_order_release);
}
if (clientConnection != nullptr && api != nullptr) {
api->ConnectionClose(clientConnection);
}
if (api != nullptr) {
HQUIC serverConnection =
state.ServerConnection.exchange(nullptr, std::memory_order_acq_rel);
if (serverConnection != nullptr) {
api->ConnectionClose(serverConnection);
}
}
if (listener != nullptr && api != nullptr) {
api->ListenerClose(listener);
}
if (clientConfiguration != nullptr && api != nullptr) {
api->ConfigurationClose(clientConfiguration);
}
if (serverConfiguration != nullptr && api != nullptr) {
api->ConfigurationClose(serverConfiguration);
}
if (registration != nullptr && api != nullptr) {
api->RegistrationClose(registration);
}
if (api != nullptr) {
MsQuicClose(api);
}
return result;
}
Stream callbacks should not be delivered on streams that was never started successfully.
Application receives callbacks for the stream being closed even when the stream was never started successfully.
Describe the bug
If StreamStart is called after the connection has received shutdown-complete, StreamStart fails with 0x80004004, yet the application still receives callbacks that were stated to never be delivered.
Documentation explicitly indicates no callbacks should be delivered for such streams,
StreamStart function
Affected OS
Additional OS information
Windows Version 25H2 OS Build 26200.8893
MsQuic version
v2.5.6
Steps taken to reproduce bug
Asked AI to generate a quick example repro.:
note using
QUIC_STREAM_START_FLAG_SHUTDOWN_ON_FAILdoes not change the behavior,Expected behavior
Stream callbacks should not be delivered on streams that was never started successfully.
Actual outcome
Application receives callbacks for the stream being closed even when the stream was never started successfully.
Additional details
Please also clarify if
QUIC_STREAM_EVENT_START_COMPLETEwould ever be delivered whenStreamStartfails synchronously. The wording today reads to me as though it should not be delivered.