From e6f911e3f288008bd1343ac0242b01ac1e29f964 Mon Sep 17 00:00:00 2001 From: Justin M Date: Mon, 29 Jun 2026 17:08:49 -0700 Subject: [PATCH 1/9] Add ExtensionEventLogger facade for session-aware XEvent logging Expose a public, session-aware logging facade (ExtensionEventLogger + ExtensionTraceLevel) so .NET Core C# extensions can emit traces through the host's Extended Events infrastructure (SQLExtension.extension_trace_event) without handling session/task ids or the internal host-callback plumbing. The facade resolves the current session id/task id from the active CSharpSession and calls the internal Logging.LogXEvent. New internal accessors (CSharpExtension.CurrentSession, CSharpSession.SessionId/TaskId) support it. No-op and never throws when the host has not registered a callback. --- .../src/managed/CSharpExtension.cs | 8 ++ .../src/managed/CSharpSession.cs | 12 ++ .../src/managed/sdk/ExtensionEventLogger.cs | 126 ++++++++++++++++++ 3 files changed, 146 insertions(+) create mode 100644 language-extensions/dotnet-core-CSharp/src/managed/sdk/ExtensionEventLogger.cs diff --git a/language-extensions/dotnet-core-CSharp/src/managed/CSharpExtension.cs b/language-extensions/dotnet-core-CSharp/src/managed/CSharpExtension.cs index 870ec459..2c66ccea 100644 --- a/language-extensions/dotnet-core-CSharp/src/managed/CSharpExtension.cs +++ b/language-extensions/dotnet-core-CSharp/src/managed/CSharpExtension.cs @@ -30,6 +30,14 @@ public static unsafe class CSharpExtension /// private static CSharpSession _currentSession; + /// + /// The session currently being executed, or null before InitSession / after + /// CleanupSession. Exposed at internal scope so the SDK logging facade + /// (ExtensionEventLogger) can attribute emitted XEvents to the active + /// session/task without the extension author having to thread the ids through. + /// + internal static CSharpSession CurrentSession => _currentSession; + /// /// The absolute path to the installation directory of the extension. /// diff --git a/language-extensions/dotnet-core-CSharp/src/managed/CSharpSession.cs b/language-extensions/dotnet-core-CSharp/src/managed/CSharpSession.cs index 8d396edb..d4e12706 100644 --- a/language-extensions/dotnet-core-CSharp/src/managed/CSharpSession.cs +++ b/language-extensions/dotnet-core-CSharp/src/managed/CSharpSession.cs @@ -123,6 +123,18 @@ public CSharpSession( _paramContainer = new CSharpParamContainer(parametersNumber); } + /// + /// GUID identifying this session. Exposed at internal scope so the SDK + /// logging facade can attribute emitted XEvents to this session. + /// + internal Guid SessionId => _sessionId; + + /// + /// Task identifier for this session. Exposed at internal scope so the SDK + /// logging facade can attribute emitted XEvents to this task. + /// + internal ushort TaskId => _taskId; + /// /// This method initializes the input column for this session. /// diff --git a/language-extensions/dotnet-core-CSharp/src/managed/sdk/ExtensionEventLogger.cs b/language-extensions/dotnet-core-CSharp/src/managed/sdk/ExtensionEventLogger.cs new file mode 100644 index 00000000..66d8e6aa --- /dev/null +++ b/language-extensions/dotnet-core-CSharp/src/managed/sdk/ExtensionEventLogger.cs @@ -0,0 +1,126 @@ +//********************************************************************* +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +// +// @File: ExtensionEventLogger.cs +// +// Purpose: +// Public, session-aware logging facade for .NET Core C# extensions. Lets an +// extension emit a trace through the host's Extended Events (XEvent) +// infrastructure so end users can observe extension activity from an XEvent +// session, without the extension needing the session/task identifiers or the +// internal host-callback plumbing. +// +//********************************************************************* +using System; + +namespace Microsoft.SqlServer.CSharpExtension.SDK +{ + /// + /// Severity of an event emitted via . + /// Lowest numeric value is the most severe, matching the Windows ETW + /// TRACE_LEVEL_* convention used by the host. + /// + public enum ExtensionTraceLevel : ushort + { + /// Critical failure. + Critical = 1, + + /// Error. + Error = 2, + + /// Warning. + Warning = 3, + + /// Informational message. + Information = 4, + + /// Verbose diagnostic detail. + Verbose = 5, + } + + /// + /// Emits traces from a .NET Core C# extension through the host's XEvent + /// infrastructure (fires SQLExtension.extension_trace_event in the + /// ExtHost satellite, which forwards to the SQL Server engine). + /// + /// + /// Availability is host-dependent: the host must implement Extension API + /// version 3 or later and register the LogXEvent callback via + /// SetHostCallbacks. When the host has not registered a callback, + /// is false and is a + /// silent no-op, so callers can invoke it unconditionally. + /// + /// Events are attributed to the session/task currently executing on this + /// satellite. If no session is active (before InitSession or after + /// CleanupSession) the event is emitted with an empty session id; + /// the host drops empty-session events before they reach the engine, so + /// they are not visible to end-user XEvent sessions. + /// + public static class ExtensionEventLogger + { + /// + /// True when the host has registered an XEvent logging callback and + /// emitted events can reach the host. When false, + /// does nothing. + /// + public static bool IsAvailable => Logging.HasLogXEventCallback; + + /// + /// Emit a trace through the host's XEvent infrastructure, attributed to + /// the session/task currently executing on this satellite. Safe to call + /// even when no host callback is registered (no-op) and never throws. + /// + /// Severity of the event. + /// + /// Message text. Callers are responsible for excluding secrets and other + /// sensitive payloads; the host records this verbatim. + /// + /// + /// Optional error code associated with the event. Defaults to 0. + /// + /// + /// Optional name identifying the originating extension. When null or + /// empty the host substitutes a default. + /// + public static void Log( + ExtensionTraceLevel level, + string message, + int errorCode = 0, + string extensionName = null) + { + // Snapshot the current session once so concurrent cleanup between the + // read and the use cannot turn the field accesses into a race. + CSharpSession session = CSharpExtension.CurrentSession; + + Guid sessionId = session?.SessionId ?? Guid.Empty; + ushort taskId = session?.TaskId ?? 0; + + Logging.LogXEvent( + extensionName, + sessionId, + taskId, + ToLoggingTraceLevel(level), + errorCode, + message); + } + + /// + /// Map the public severity to the internal logging severity. The two + /// enums share numeric values, but mapping explicitly keeps the public + /// API decoupled from the internal type. + /// + private static Logging.TraceLevel ToLoggingTraceLevel(ExtensionTraceLevel level) + { + switch (level) + { + case ExtensionTraceLevel.Critical: return Logging.TraceLevel.Critical; + case ExtensionTraceLevel.Error: return Logging.TraceLevel.Error; + case ExtensionTraceLevel.Warning: return Logging.TraceLevel.Warning; + case ExtensionTraceLevel.Verbose: return Logging.TraceLevel.Verbose; + case ExtensionTraceLevel.Information: return Logging.TraceLevel.Information; + default: return Logging.TraceLevel.Information; + } + } + } +} From b9999830cd2abb9015fa7fe20c4461770ff6041f Mon Sep 17 00:00:00 2001 From: Justin M Date: Tue, 30 Jun 2026 13:27:35 -0700 Subject: [PATCH 2/9] Trim comments to self-documenting style Remove per-member enum docs and verbose remarks/inline blocks; keep concise public-API docs and only non-obvious rationale. --- .../src/managed/CSharpExtension.cs | 8 +-- .../src/managed/CSharpSession.cs | 9 +-- .../src/managed/sdk/ExtensionEventLogger.cs | 67 +++++-------------- 3 files changed, 20 insertions(+), 64 deletions(-) diff --git a/language-extensions/dotnet-core-CSharp/src/managed/CSharpExtension.cs b/language-extensions/dotnet-core-CSharp/src/managed/CSharpExtension.cs index 2c66ccea..b0028abb 100644 --- a/language-extensions/dotnet-core-CSharp/src/managed/CSharpExtension.cs +++ b/language-extensions/dotnet-core-CSharp/src/managed/CSharpExtension.cs @@ -30,12 +30,8 @@ public static unsafe class CSharpExtension /// private static CSharpSession _currentSession; - /// - /// The session currently being executed, or null before InitSession / after - /// CleanupSession. Exposed at internal scope so the SDK logging facade - /// (ExtensionEventLogger) can attribute emitted XEvents to the active - /// session/task without the extension author having to thread the ids through. - /// + // Active session (null before InitSession / after CleanupSession). Internal + // so the SDK logging facade can attribute events without threading the ids. internal static CSharpSession CurrentSession => _currentSession; /// diff --git a/language-extensions/dotnet-core-CSharp/src/managed/CSharpSession.cs b/language-extensions/dotnet-core-CSharp/src/managed/CSharpSession.cs index d4e12706..aec1c6be 100644 --- a/language-extensions/dotnet-core-CSharp/src/managed/CSharpSession.cs +++ b/language-extensions/dotnet-core-CSharp/src/managed/CSharpSession.cs @@ -123,16 +123,9 @@ public CSharpSession( _paramContainer = new CSharpParamContainer(parametersNumber); } - /// - /// GUID identifying this session. Exposed at internal scope so the SDK - /// logging facade can attribute emitted XEvents to this session. - /// + // Exposed internally so the SDK logging facade can attribute XEvents. internal Guid SessionId => _sessionId; - /// - /// Task identifier for this session. Exposed at internal scope so the SDK - /// logging facade can attribute emitted XEvents to this task. - /// internal ushort TaskId => _taskId; /// diff --git a/language-extensions/dotnet-core-CSharp/src/managed/sdk/ExtensionEventLogger.cs b/language-extensions/dotnet-core-CSharp/src/managed/sdk/ExtensionEventLogger.cs index 66d8e6aa..6d1141da 100644 --- a/language-extensions/dotnet-core-CSharp/src/managed/sdk/ExtensionEventLogger.cs +++ b/language-extensions/dotnet-core-CSharp/src/managed/sdk/ExtensionEventLogger.cs @@ -17,80 +17,51 @@ namespace Microsoft.SqlServer.CSharpExtension.SDK { /// - /// Severity of an event emitted via . - /// Lowest numeric value is the most severe, matching the Windows ETW - /// TRACE_LEVEL_* convention used by the host. + /// Severity of an event emitted via . Lowest + /// value is most severe, matching the host's ETW TRACE_LEVEL_* convention. /// public enum ExtensionTraceLevel : ushort { - /// Critical failure. Critical = 1, - - /// Error. Error = 2, - - /// Warning. Warning = 3, - - /// Informational message. Information = 4, - - /// Verbose diagnostic detail. Verbose = 5, } /// - /// Emits traces from a .NET Core C# extension through the host's XEvent - /// infrastructure (fires SQLExtension.extension_trace_event in the - /// ExtHost satellite, which forwards to the SQL Server engine). + /// Emits traces from a C# extension through the host's XEvent infrastructure + /// (fires SQLExtension.extension_trace_event, forwarded to the engine). /// /// - /// Availability is host-dependent: the host must implement Extension API - /// version 3 or later and register the LogXEvent callback via - /// SetHostCallbacks. When the host has not registered a callback, - /// is false and is a - /// silent no-op, so callers can invoke it unconditionally. - /// - /// Events are attributed to the session/task currently executing on this - /// satellite. If no session is active (before InitSession or after - /// CleanupSession) the event is emitted with an empty session id; - /// the host drops empty-session events before they reach the engine, so - /// they are not visible to end-user XEvent sessions. + /// Safe to call unconditionally: is a silent no-op when the + /// host has registered no callback (see ). Events are + /// attributed to the session executing on this satellite; when no session is + /// active the host drops the event, so it is not visible to end users. /// public static class ExtensionEventLogger { /// - /// True when the host has registered an XEvent logging callback and - /// emitted events can reach the host. When false, - /// does nothing. + /// True when the host has registered a callback and emitted events can + /// reach it. When false, does nothing. /// public static bool IsAvailable => Logging.HasLogXEventCallback; /// - /// Emit a trace through the host's XEvent infrastructure, attributed to - /// the session/task currently executing on this satellite. Safe to call - /// even when no host callback is registered (no-op) and never throws. + /// Emit a trace through the host's XEvent infrastructure, attributed to the + /// current session/task. Safe to call with no host callback (no-op); never throws. /// /// Severity of the event. - /// - /// Message text. Callers are responsible for excluding secrets and other - /// sensitive payloads; the host records this verbatim. - /// - /// - /// Optional error code associated with the event. Defaults to 0. - /// - /// - /// Optional name identifying the originating extension. When null or - /// empty the host substitutes a default. - /// + /// Message text; callers must exclude secrets — recorded verbatim. + /// Optional error code. Defaults to 0. + /// Optional originating extension name; host substitutes a default when empty. public static void Log( ExtensionTraceLevel level, string message, int errorCode = 0, string extensionName = null) { - // Snapshot the current session once so concurrent cleanup between the - // read and the use cannot turn the field accesses into a race. + // Snapshot once so a concurrent cleanup can't race the field reads. CSharpSession session = CSharpExtension.CurrentSession; Guid sessionId = session?.SessionId ?? Guid.Empty; @@ -105,11 +76,7 @@ public static void Log( message); } - /// - /// Map the public severity to the internal logging severity. The two - /// enums share numeric values, but mapping explicitly keeps the public - /// API decoupled from the internal type. - /// + // Map to the internal severity by name to keep the public enum decoupled. private static Logging.TraceLevel ToLoggingTraceLevel(ExtensionTraceLevel level) { switch (level) From bc861b2b4964bc49f126ab7dd87e162806d92f77 Mon Sep 17 00:00:00 2001 From: Justin M Date: Mon, 13 Jul 2026 14:08:09 -0700 Subject: [PATCH 3/9] Add ExtensionEventLogger end-to-end tests Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: f1049918-fdbb-43f3-aece-22bc42c2801c --- .../test/src/managed/CSharpTestExecutor.cs | 17 +++ .../native/CSharpSetHostCallbacksTests.cpp | 108 +++++++++++++++++- 2 files changed, 121 insertions(+), 4 deletions(-) diff --git a/language-extensions/dotnet-core-CSharp/test/src/managed/CSharpTestExecutor.cs b/language-extensions/dotnet-core-CSharp/test/src/managed/CSharpTestExecutor.cs index ffb2c3f7..53fe1a70 100644 --- a/language-extensions/dotnet-core-CSharp/test/src/managed/CSharpTestExecutor.cs +++ b/language-extensions/dotnet-core-CSharp/test/src/managed/CSharpTestExecutor.cs @@ -38,6 +38,23 @@ public override DataFrame Execute(DataFrame input, Dictionary s } } + public class CSharpTestExecutorExtensionEventLogger: AbstractSqlServerExtensionExecutor + { + public override DataFrame Execute(DataFrame input, Dictionary sqlParams) + { + ExtensionEventLogger.Log( + ExtensionTraceLevel.Critical, + "critical event", + 101, + "TestExtension"); + ExtensionEventLogger.Log(ExtensionTraceLevel.Error, "error event", 102); + ExtensionEventLogger.Log(ExtensionTraceLevel.Warning, "warning event", 103); + ExtensionEventLogger.Log(ExtensionTraceLevel.Information, "information event", 104); + ExtensionEventLogger.Log(ExtensionTraceLevel.Verbose, "verbose event", 105); + return input; + } + } + public class CSharpTestExecutorIntParam: AbstractSqlServerExtensionExecutor { public override DataFrame Execute(DataFrame input, Dictionary sqlParams){ diff --git a/language-extensions/dotnet-core-CSharp/test/src/native/CSharpSetHostCallbacksTests.cpp b/language-extensions/dotnet-core-CSharp/test/src/native/CSharpSetHostCallbacksTests.cpp index c2968d51..8d242ea4 100644 --- a/language-extensions/dotnet-core-CSharp/test/src/native/CSharpSetHostCallbacksTests.cpp +++ b/language-extensions/dotnet-core-CSharp/test/src/native/CSharpSetHostCallbacksTests.cpp @@ -30,6 +30,8 @@ namespace ExtensionApiTest struct CapturedLogEvent { string extensionName; + SQLGUID sessionId; + SQLUSMALLINT taskId; SQLUSMALLINT traceLevel; SQLINTEGER errorCode; string message; @@ -60,6 +62,8 @@ namespace ExtensionApiTest reinterpret_cast(extensionName), static_cast(extensionNameLength)); } + ev.sessionId = sessionId; + ev.taskId = taskId; ev.traceLevel = traceLevel; ev.errorCode = errorCode; if (message != nullptr && messageLength > 0) @@ -123,8 +127,9 @@ namespace ExtensionApiTest g_capturedLogEvents.clear(); SQLEXTENSION_HOST_CALLBACKS hostCallbacks{}; - hostCallbacks.Version = SQLEXTENSION_HOST_CALLBACKS_VERSION_1; - hostCallbacks.LogXEvent = &TestLogXEventCallback; + hostCallbacks.Version = SQLEXTENSION_HOST_CALLBACKS_VERSION_1; + hostCallbacks.SizeInBytes = sizeof(hostCallbacks); + hostCallbacks.LogXEvent = &TestLogXEventCallback; SQLRETURN rc = fn(&hostCallbacks); EXPECT_EQ(rc, SQL_SUCCESS); @@ -141,6 +146,100 @@ namespace ExtensionApiTest << "Unexpected message: " << ev.message; } + TEST_F(CSharpExtensionApiTests, ExtensionEventLogger_ForwardsSessionEvent) + { + FN_setHostCallbacks *fn = RESOLVE_SET_HOST_CALLBACKS(); + ASSERT_NE(fn, nullptr); + + SQLEXTENSION_HOST_CALLBACKS hostCallbacks{}; + hostCallbacks.Version = SQLEXTENSION_HOST_CALLBACKS_VERSION_1; + hostCallbacks.SizeInBytes = sizeof(hostCallbacks); + hostCallbacks.LogXEvent = &TestLogXEventCallback; + + ASSERT_EQ(fn(&hostCallbacks), SQL_SUCCESS); + g_capturedLogEvents.clear(); + + m_sessionId->Data1 = 0x01234567; + m_sessionId->Data2 = 0x89AB; + m_sessionId->Data3 = 0xCDEF; + const unsigned char data4[] = { 0x01, 0x23, 0x45, 0x67, 0x89, 0xAB, 0xCD, 0xEF }; + memcpy(m_sessionId->Data4, data4, sizeof(data4)); + m_taskId = 42; + + string script = m_UserLibName + m_Separator + + "Microsoft.SqlServer.CSharpExtensionTest.CSharpTestExecutorExtensionEventLogger"; + InitializeSession(0, 0, script); + + SQLUSMALLINT outputSchemaColumnsNumber = 0; + ASSERT_EQ( + (*sm_executeFuncPtr)( + *m_sessionId, + m_taskId, + 0, + nullptr, + nullptr, + &outputSchemaColumnsNumber), + SQL_SUCCESS); + + ASSERT_EQ(g_capturedLogEvents.size(), 5); + + const SQLUSMALLINT expectedLevels[] = { + Extension_Critical, + Extension_Error, + Extension_Warning, + Extension_Information, + Extension_Verbose + }; + const SQLINTEGER expectedErrorCodes[] = { 101, 102, 103, 104, 105 }; + const string expectedMessages[] = { + "critical event", + "error event", + "warning event", + "information event", + "verbose event" + }; + + for (size_t i = 0; i < g_capturedLogEvents.size(); ++i) + { + const CapturedLogEvent &event = g_capturedLogEvents[i]; + EXPECT_EQ(memcmp(&event.sessionId, m_sessionId.get(), sizeof(SQLGUID)), 0); + EXPECT_EQ(event.taskId, m_taskId); + EXPECT_EQ(event.traceLevel, expectedLevels[i]); + EXPECT_EQ(event.errorCode, expectedErrorCodes[i]); + EXPECT_EQ(event.message, expectedMessages[i]); + EXPECT_EQ(event.extensionName, i == 0 ? "TestExtension" : "CSharp"); + } + } + + TEST_F(CSharpExtensionApiTests, ExtensionEventLogger_NoCallbackIsNoOp) + { + FN_setHostCallbacks *fn = RESOLVE_SET_HOST_CALLBACKS(); + ASSERT_NE(fn, nullptr); + + SQLEXTENSION_HOST_CALLBACKS hostCallbacks{}; + hostCallbacks.Version = SQLEXTENSION_HOST_CALLBACKS_VERSION_1; + hostCallbacks.SizeInBytes = sizeof(hostCallbacks); + hostCallbacks.LogXEvent = nullptr; + ASSERT_EQ(fn(&hostCallbacks), SQL_SUCCESS); + g_capturedLogEvents.clear(); + + string script = m_UserLibName + m_Separator + + "Microsoft.SqlServer.CSharpExtensionTest.CSharpTestExecutorExtensionEventLogger"; + InitializeSession(0, 0, script); + + SQLUSMALLINT outputSchemaColumnsNumber = 0; + EXPECT_EQ( + (*sm_executeFuncPtr)( + *m_sessionId, + m_taskId, + 0, + nullptr, + nullptr, + &outputSchemaColumnsNumber), + SQL_SUCCESS); + EXPECT_TRUE(g_capturedLogEvents.empty()); + } + //---------------------------------------------------------------------------------------------- // Name: SetHostCallbacks_NullLogXEventIsAccepted // @@ -157,8 +256,9 @@ namespace ExtensionApiTest g_capturedLogEvents.clear(); SQLEXTENSION_HOST_CALLBACKS hostCallbacks{}; - hostCallbacks.Version = SQLEXTENSION_HOST_CALLBACKS_VERSION_1; - hostCallbacks.LogXEvent = nullptr; + hostCallbacks.Version = SQLEXTENSION_HOST_CALLBACKS_VERSION_1; + hostCallbacks.SizeInBytes = sizeof(hostCallbacks); + hostCallbacks.LogXEvent = nullptr; SQLRETURN rc = fn(&hostCallbacks); EXPECT_EQ(rc, SQL_SUCCESS); From 06242165b269104985459ee191b60f23b87ea3cd Mon Sep 17 00:00:00 2001 From: Justin M Date: Thu, 16 Jul 2026 10:42:04 -0700 Subject: [PATCH 4/9] Validate host callback structure size Reject incomplete callback structures, normalize the native copy before forwarding it to managed code, and cover minimum-v1 compatibility. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: f1049918-fdbb-43f3-aece-22bc42c2801c --- .../src/managed/CSharpExtension.cs | 8 ++++ .../src/native/nativecsharpextension.cpp | 26 +++++++++---- .../native/CSharpSetHostCallbacksTests.cpp | 38 +++++++++++++++++++ 3 files changed, 64 insertions(+), 8 deletions(-) diff --git a/language-extensions/dotnet-core-CSharp/src/managed/CSharpExtension.cs b/language-extensions/dotnet-core-CSharp/src/managed/CSharpExtension.cs index 7bf755d6..98bb0a1a 100644 --- a/language-extensions/dotnet-core-CSharp/src/managed/CSharpExtension.cs +++ b/language-extensions/dotnet-core-CSharp/src/managed/CSharpExtension.cs @@ -742,6 +742,14 @@ public static short SetHostCallbacks( hostCallbacks->Version); } + if (hostCallbacks->SizeInBytes < Marshal.SizeOf()) + { + Logging.Error("CSharpExtension::SetHostCallbacks: incomplete host callbacks structure"); + throw new ArgumentException( + "Incomplete SQLEXTENSION_HOST_CALLBACKS structure.", + nameof(hostCallbacks)); + } + if (hostCallbacks->LogXEvent != IntPtr.Zero) { var logXEvent = Marshal.GetDelegateForFunctionPointer( diff --git a/language-extensions/dotnet-core-CSharp/src/native/nativecsharpextension.cpp b/language-extensions/dotnet-core-CSharp/src/native/nativecsharpextension.cpp index 0ef1ce39..cdfdd0d6 100644 --- a/language-extensions/dotnet-core-CSharp/src/native/nativecsharpextension.cpp +++ b/language-extensions/dotnet-core-CSharp/src/native/nativecsharpextension.cpp @@ -359,8 +359,9 @@ SQLRETURN Cleanup() if (g_dotnet_runtime != nullptr) { SQLEXTENSION_HOST_CALLBACKS nullCallbacks = {}; - nullCallbacks.Version = SQLEXTENSION_HOST_CALLBACKS_VERSION_1; - nullCallbacks.LogXEvent = nullptr; + nullCallbacks.Version = SQLEXTENSION_HOST_CALLBACKS_VERSION_1; + nullCallbacks.SizeInBytes = sizeof(nullCallbacks); + nullCallbacks.LogXEvent = nullptr; g_dotnet_runtime->call_managed_method( nameof(SetHostCallbacks), @@ -563,13 +564,22 @@ SQLRETURN SetHostCallbacks( return SQL_ERROR; } - // Take a shallow copy of the caller's struct so g_hostCallbacks cannot - // dangle if the host passed a stack-allocated SQLEXTENSION_HOST_CALLBACKS. - // - g_hostCallbacksCopy = *hostCallbacks; + const SQLUINTEGER minHostCallbacksSize = + static_cast(offsetof(SQLEXTENSION_HOST_CALLBACKS, LogXEvent) + sizeof(hostCallbacks->LogXEvent)); + if (hostCallbacks->SizeInBytes < minHostCallbacksSize) + { + LOG_ERROR("SetHostCallbacks called with incomplete host callbacks structure"); + return SQL_ERROR; + } + + g_hostCallbacksCopy = {}; + g_hostCallbacksCopy.Version = hostCallbacks->Version; + g_hostCallbacksCopy.Reserved0 = hostCallbacks->Reserved0; + g_hostCallbacksCopy.SizeInBytes = sizeof(g_hostCallbacksCopy); + g_hostCallbacksCopy.LogXEvent = hostCallbacks->LogXEvent; g_hostCallbacks = &g_hostCallbacksCopy; return g_dotnet_runtime->call_managed_method( nameof(SetHostCallbacks), - hostCallbacks); -} \ No newline at end of file + &g_hostCallbacksCopy); +} diff --git a/language-extensions/dotnet-core-CSharp/test/src/native/CSharpSetHostCallbacksTests.cpp b/language-extensions/dotnet-core-CSharp/test/src/native/CSharpSetHostCallbacksTests.cpp index 8d242ea4..c965eadc 100644 --- a/language-extensions/dotnet-core-CSharp/test/src/native/CSharpSetHostCallbacksTests.cpp +++ b/language-extensions/dotnet-core-CSharp/test/src/native/CSharpSetHostCallbacksTests.cpp @@ -13,6 +13,7 @@ //********************************************************************* #include "CSharpExtensionApiTests.h" +#include #include #include #include @@ -110,6 +111,43 @@ namespace ExtensionApiTest EXPECT_EQ(rc, SQL_ERROR); } + TEST_F(CSharpExtensionApiTests, SetHostCallbacks_IncompleteStructureReturnsError) + { + FN_setHostCallbacks *fn = RESOLVE_SET_HOST_CALLBACKS(); + ASSERT_NE(fn, nullptr); + + SQLEXTENSION_HOST_CALLBACKS hostCallbacks{}; + hostCallbacks.Version = SQLEXTENSION_HOST_CALLBACKS_VERSION_1; + hostCallbacks.SizeInBytes = offsetof(SQLEXTENSION_HOST_CALLBACKS, LogXEvent); + hostCallbacks.LogXEvent = &TestLogXEventCallback; + + EXPECT_EQ(fn(&hostCallbacks), SQL_ERROR); + } + + TEST_F(CSharpExtensionApiTests, SetHostCallbacks_MinimumV1StructureIsAccepted) + { + FN_setHostCallbacks *fn = RESOLVE_SET_HOST_CALLBACKS(); + ASSERT_NE(fn, nullptr); + + struct V1HostCallbacks + { + SQLUSMALLINT Version; + SQLUSMALLINT Reserved0; + SQLUINTEGER SizeInBytes; + PFunc_ExtensionLogXEvent LogXEvent; + }; + + g_capturedLogEvents.clear(); + + V1HostCallbacks hostCallbacks{}; + hostCallbacks.Version = SQLEXTENSION_HOST_CALLBACKS_VERSION_1; + hostCallbacks.SizeInBytes = sizeof(hostCallbacks); + hostCallbacks.LogXEvent = &TestLogXEventCallback; + + EXPECT_EQ(fn(reinterpret_cast(&hostCallbacks)), SQL_SUCCESS); + EXPECT_FALSE(g_capturedLogEvents.empty()); + } + //---------------------------------------------------------------------------------------------- // Name: SetHostCallbacks_RegistersAndForwardsLogXEvent // From 9b8d65f364aa9bdd0792d146bbda821a7b4ff149 Mon Sep 17 00:00:00 2001 From: Justin M Date: Thu, 16 Jul 2026 12:13:36 -0700 Subject: [PATCH 5/9] Simplify ExtensionEventLogger documentation Keep public API behavior documented while removing comments that restate the code. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: f1049918-fdbb-43f3-aece-22bc42c2801c --- .../src/managed/CSharpExtension.cs | 2 -- .../src/managed/CSharpSession.cs | 1 - .../src/managed/sdk/ExtensionEventLogger.cs | 32 ++++++------------- 3 files changed, 10 insertions(+), 25 deletions(-) diff --git a/language-extensions/dotnet-core-CSharp/src/managed/CSharpExtension.cs b/language-extensions/dotnet-core-CSharp/src/managed/CSharpExtension.cs index 98bb0a1a..a6a52092 100644 --- a/language-extensions/dotnet-core-CSharp/src/managed/CSharpExtension.cs +++ b/language-extensions/dotnet-core-CSharp/src/managed/CSharpExtension.cs @@ -30,8 +30,6 @@ public static unsafe class CSharpExtension /// private static CSharpSession _currentSession; - // Active session (null before InitSession / after CleanupSession). Internal - // so the SDK logging facade can attribute events without threading the ids. internal static CSharpSession CurrentSession => _currentSession; /// diff --git a/language-extensions/dotnet-core-CSharp/src/managed/CSharpSession.cs b/language-extensions/dotnet-core-CSharp/src/managed/CSharpSession.cs index edb7a680..010079e9 100644 --- a/language-extensions/dotnet-core-CSharp/src/managed/CSharpSession.cs +++ b/language-extensions/dotnet-core-CSharp/src/managed/CSharpSession.cs @@ -123,7 +123,6 @@ public CSharpSession( _paramContainer = new CSharpParamContainer(parametersNumber); } - // Exposed internally so the SDK logging facade can attribute XEvents. internal Guid SessionId => _sessionId; internal ushort TaskId => _taskId; diff --git a/language-extensions/dotnet-core-CSharp/src/managed/sdk/ExtensionEventLogger.cs b/language-extensions/dotnet-core-CSharp/src/managed/sdk/ExtensionEventLogger.cs index 6d1141da..7f867f55 100644 --- a/language-extensions/dotnet-core-CSharp/src/managed/sdk/ExtensionEventLogger.cs +++ b/language-extensions/dotnet-core-CSharp/src/managed/sdk/ExtensionEventLogger.cs @@ -5,11 +5,7 @@ // @File: ExtensionEventLogger.cs // // Purpose: -// Public, session-aware logging facade for .NET Core C# extensions. Lets an -// extension emit a trace through the host's Extended Events (XEvent) -// infrastructure so end users can observe extension activity from an XEvent -// session, without the extension needing the session/task identifiers or the -// internal host-callback plumbing. +// Provides session-aware XEvent logging for .NET Core C# extensions. // //********************************************************************* using System; @@ -17,8 +13,7 @@ namespace Microsoft.SqlServer.CSharpExtension.SDK { /// - /// Severity of an event emitted via . Lowest - /// value is most severe, matching the host's ETW TRACE_LEVEL_* convention. + /// Severity of an extension trace event. /// public enum ExtensionTraceLevel : ushort { @@ -30,38 +25,32 @@ public enum ExtensionTraceLevel : ushort } /// - /// Emits traces from a C# extension through the host's XEvent infrastructure - /// (fires SQLExtension.extension_trace_event, forwarded to the engine). + /// Emits session-attributed traces through the host's XEvent infrastructure. /// /// - /// Safe to call unconditionally: is a silent no-op when the - /// host has registered no callback (see ). Events are - /// attributed to the session executing on this satellite; when no session is - /// active the host drops the event, so it is not visible to end users. + /// does nothing when no host callback is registered. Events + /// emitted without an active session are not visible to end users. /// public static class ExtensionEventLogger { /// - /// True when the host has registered a callback and emitted events can - /// reach it. When false, does nothing. + /// Gets whether the host accepts extension trace events. /// public static bool IsAvailable => Logging.HasLogXEventCallback; /// - /// Emit a trace through the host's XEvent infrastructure, attributed to the - /// current session/task. Safe to call with no host callback (no-op); never throws. + /// Emits a trace attributed to the current session and task. /// /// Severity of the event. - /// Message text; callers must exclude secrets — recorded verbatim. - /// Optional error code. Defaults to 0. - /// Optional originating extension name; host substitutes a default when empty. + /// Message recorded verbatim; callers must exclude secrets. + /// Optional error code. + /// Optional originating extension name. public static void Log( ExtensionTraceLevel level, string message, int errorCode = 0, string extensionName = null) { - // Snapshot once so a concurrent cleanup can't race the field reads. CSharpSession session = CSharpExtension.CurrentSession; Guid sessionId = session?.SessionId ?? Guid.Empty; @@ -76,7 +65,6 @@ public static void Log( message); } - // Map to the internal severity by name to keep the public enum decoupled. private static Logging.TraceLevel ToLoggingTraceLevel(ExtensionTraceLevel level) { switch (level) From 3fef9c6ee01774283f7e2d8e94c7e1c1cd05997f Mon Sep 17 00:00:00 2001 From: Justin M Date: Thu, 16 Jul 2026 12:15:17 -0700 Subject: [PATCH 6/9] Align managed callback size validation Validate through the last callback field consumed instead of requiring the full reserved structure. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: f1049918-fdbb-43f3-aece-22bc42c2801c --- .../dotnet-core-CSharp/src/managed/CSharpExtension.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/language-extensions/dotnet-core-CSharp/src/managed/CSharpExtension.cs b/language-extensions/dotnet-core-CSharp/src/managed/CSharpExtension.cs index a6a52092..dd503edc 100644 --- a/language-extensions/dotnet-core-CSharp/src/managed/CSharpExtension.cs +++ b/language-extensions/dotnet-core-CSharp/src/managed/CSharpExtension.cs @@ -698,6 +698,10 @@ public struct SqlExtensionHostCallbacks /// private const ushort MinSupportedHostCallbacksVersion = 1; + private static readonly uint MinimumHostCallbacksSize = checked( + (uint)(Marshal.OffsetOf( + nameof(SqlExtensionHostCallbacks.LogXEvent)).ToInt64() + IntPtr.Size)); + /// /// This delegate declares the delegate type of SetHostCallbacks. /// @@ -740,7 +744,7 @@ public static short SetHostCallbacks( hostCallbacks->Version); } - if (hostCallbacks->SizeInBytes < Marshal.SizeOf()) + if (hostCallbacks->SizeInBytes < MinimumHostCallbacksSize) { Logging.Error("CSharpExtension::SetHostCallbacks: incomplete host callbacks structure"); throw new ArgumentException( From b46bb1575bafd623b7c3a25856dd0134eed92368 Mon Sep 17 00:00:00 2001 From: Justin M Date: Tue, 21 Jul 2026 14:04:25 -0700 Subject: [PATCH 7/9] Remove unused session getters superseded by AsyncLocal tagging CSharpExtension.CurrentSession and CSharpSession.SessionId/TaskId were added for the original ExtensionEventLogger facade, which read the executing session directly. The merged upstream design tags events via Logging.SetCurrentSession/LogXEventFromCurrentSession (AsyncLocal), so these internal getters have no callers. The public SDK facade Calvin consumes (ExtensionEventLogger.Log*) is unaffected. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 6049e5e5-cec7-4976-b2b1-e84465aa9974 --- .../dotnet-core-CSharp/src/managed/CSharpExtension.cs | 2 -- .../dotnet-core-CSharp/src/managed/CSharpSession.cs | 4 ---- 2 files changed, 6 deletions(-) diff --git a/language-extensions/dotnet-core-CSharp/src/managed/CSharpExtension.cs b/language-extensions/dotnet-core-CSharp/src/managed/CSharpExtension.cs index 5b7c6d74..fd1346d2 100644 --- a/language-extensions/dotnet-core-CSharp/src/managed/CSharpExtension.cs +++ b/language-extensions/dotnet-core-CSharp/src/managed/CSharpExtension.cs @@ -31,8 +31,6 @@ public static unsafe class CSharpExtension /// private static CSharpSession _currentSession; - internal static CSharpSession CurrentSession => _currentSession; - /// /// The absolute path to the installation directory of the extension. /// diff --git a/language-extensions/dotnet-core-CSharp/src/managed/CSharpSession.cs b/language-extensions/dotnet-core-CSharp/src/managed/CSharpSession.cs index 2f7d7601..14be222b 100644 --- a/language-extensions/dotnet-core-CSharp/src/managed/CSharpSession.cs +++ b/language-extensions/dotnet-core-CSharp/src/managed/CSharpSession.cs @@ -123,10 +123,6 @@ public CSharpSession( _paramContainer = new CSharpParamContainer(parametersNumber); } - internal Guid SessionId => _sessionId; - - internal ushort TaskId => _taskId; - /// /// This method initializes the input column for this session. /// From 92fb7b70091b24c713b9df0c647f6aa0f78d388e Mon Sep 17 00:00:00 2001 From: Justin M Date: Tue, 21 Jul 2026 14:10:52 -0700 Subject: [PATCH 8/9] Add extensionName overload to ExtensionEventLogger facade Adds ExtensionEventLogger.Log(level, message, errorCode, extensionName) so an in-process library loaded by a user script (e.g. MssqlAI/Calvin) can attribute its XEvents to itself instead of the host extension's default name. Plumbs the name through IExtensionLogSink.Log and XEventLogSink into Logging.LogXEventFromCurrentSession; the existing 3-arg Log and convenience methods keep the default attribution. Covered by CSharpTestExecutorLogNamedExtension + ExecuteForwardsNamedExtensionLogEvent, asserting the forwarded event carries the caller-supplied name. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 6049e5e5-cec7-4976-b2b1-e84465aa9974 --- .../src/managed/sdk/ExtensionEventLogger.cs | 18 ++++++- .../src/managed/sdk/IExtensionLogSink.cs | 6 ++- .../src/managed/utils/Logging.cs | 8 ++- .../src/managed/utils/XEventLogSink.cs | 4 +- .../test/src/managed/CSharpTestExecutor.cs | 30 +++++++++++ .../test/src/native/CSharpExecuteTests.cpp | 50 +++++++++++++++++++ 6 files changed, 109 insertions(+), 7 deletions(-) diff --git a/language-extensions/dotnet-core-CSharp/src/managed/sdk/ExtensionEventLogger.cs b/language-extensions/dotnet-core-CSharp/src/managed/sdk/ExtensionEventLogger.cs index 390f5767..15bb3bad 100644 --- a/language-extensions/dotnet-core-CSharp/src/managed/sdk/ExtensionEventLogger.cs +++ b/language-extensions/dotnet-core-CSharp/src/managed/sdk/ExtensionEventLogger.cs @@ -98,9 +98,23 @@ public static void LogVerbose(string message) => /// Event severity. /// Message to log. A null message is treated as empty. /// Optional error code for non-informational events. - public static void Log(ExtensionTraceLevel level, string message, int errorCode = 0) + public static void Log(ExtensionTraceLevel level, string message, int errorCode = 0) => + Log(level, message, errorCode, extensionName: null); + + /// + /// Logs an event at the specified severity, attributed to a named extension. + /// + /// Event severity. + /// Message to log. A null message is treated as empty. + /// Error code for non-informational events; 0 when unused. + /// + /// Name recorded as the event's originating extension. When null or empty the + /// Extension's default name is used. Lets an in-process library (e.g. one loaded + /// by a user script) attribute its events to itself rather than the host Extension. + /// + public static void Log(ExtensionTraceLevel level, string message, int errorCode, string extensionName) { - Sink?.Log(level, errorCode, message); + Sink?.Log(level, errorCode, message, extensionName); } } } diff --git a/language-extensions/dotnet-core-CSharp/src/managed/sdk/IExtensionLogSink.cs b/language-extensions/dotnet-core-CSharp/src/managed/sdk/IExtensionLogSink.cs index b6fd3d5d..bb3bae94 100644 --- a/language-extensions/dotnet-core-CSharp/src/managed/sdk/IExtensionLogSink.cs +++ b/language-extensions/dotnet-core-CSharp/src/managed/sdk/IExtensionLogSink.cs @@ -34,6 +34,10 @@ internal interface IExtensionLogSink /// Event severity. /// Error code for non-informational events; 0 when unused. /// Message to log. A null message is treated as empty. - void Log(ExtensionTraceLevel level, int errorCode, string message); + /// + /// Name recorded as the event's originating extension; null or empty selects the + /// Extension's default name. + /// + void Log(ExtensionTraceLevel level, int errorCode, string message, string extensionName); } } diff --git a/language-extensions/dotnet-core-CSharp/src/managed/utils/Logging.cs b/language-extensions/dotnet-core-CSharp/src/managed/utils/Logging.cs index c1ba98b5..51a27544 100644 --- a/language-extensions/dotnet-core-CSharp/src/managed/utils/Logging.cs +++ b/language-extensions/dotnet-core-CSharp/src/managed/utils/Logging.cs @@ -138,14 +138,18 @@ internal static void ClearCurrentSession() /// Trace severity. /// Error code for non-informational logs. /// The message to log. + /// + /// Originating extension name; null or empty selects the default name. + /// internal static void LogXEventFromCurrentSession( ExtensionTraceLevel traceLevel, int errorCode, - string message) + string message, + string extensionName = null) { (Guid sessionId, ushort taskId) = _currentSession.Value; LogXEvent( - extensionName: null, + extensionName: extensionName, sessionId: sessionId, taskId: taskId, traceLevel: traceLevel, diff --git a/language-extensions/dotnet-core-CSharp/src/managed/utils/XEventLogSink.cs b/language-extensions/dotnet-core-CSharp/src/managed/utils/XEventLogSink.cs index b20f7c5a..f4ea63f9 100644 --- a/language-extensions/dotnet-core-CSharp/src/managed/utils/XEventLogSink.cs +++ b/language-extensions/dotnet-core-CSharp/src/managed/utils/XEventLogSink.cs @@ -27,7 +27,7 @@ internal sealed class XEventLogSink : IExtensionLogSink public bool IsEnabled => Logging.HasLogXEventCallback; /// - public void Log(ExtensionTraceLevel level, int errorCode, string message) => - Logging.LogXEventFromCurrentSession(level, errorCode, message); + public void Log(ExtensionTraceLevel level, int errorCode, string message, string extensionName) => + Logging.LogXEventFromCurrentSession(level, errorCode, message, extensionName); } } diff --git a/language-extensions/dotnet-core-CSharp/test/src/managed/CSharpTestExecutor.cs b/language-extensions/dotnet-core-CSharp/test/src/managed/CSharpTestExecutor.cs index ba7753a6..7825a947 100644 --- a/language-extensions/dotnet-core-CSharp/test/src/managed/CSharpTestExecutor.cs +++ b/language-extensions/dotnet-core-CSharp/test/src/managed/CSharpTestExecutor.cs @@ -36,6 +36,19 @@ internal static class CSharpTestExecutorConstants /// so both sides must stay in sync. /// public const string LogEventMessage = "CSharpTestExecutorLogInformation emitted event"; + + /// + /// Marker message emitted through the SDK ExtensionEventLogger extensionName + /// overload by CSharpTestExecutorLogNamedExtension. Matched by the native test + /// (CSharpExecuteTests.cpp) alongside . + /// + public const string LogNamedEventMessage = "CSharpTestExecutorLogNamedExtension emitted event"; + + /// + /// Extension name CSharpTestExecutorLogNamedExtension attributes its event to, + /// proving the ExtensionEventLogger.Log extensionName overload is honored end to end. + /// + public const string LogEventExtensionName = "TestExtension"; } public class CSharpTestExecutor: AbstractSqlServerExtensionExecutor @@ -60,6 +73,23 @@ public override DataFrame Execute(DataFrame input, Dictionary s } } + /// + /// Test executor that emits an event through the ExtensionEventLogger extensionName + /// overload, attributing it to a named extension. Lets the native test assert the + /// forwarded XEvent carries the caller-supplied extension name rather than the default. + /// + public class CSharpTestExecutorLogNamedExtension: AbstractSqlServerExtensionExecutor + { + public override DataFrame Execute(DataFrame input, Dictionary sqlParams){ + ExtensionEventLogger.Log( + ExtensionTraceLevel.Information, + CSharpTestExecutorConstants.LogNamedEventMessage, + errorCode: 0, + extensionName: CSharpTestExecutorConstants.LogEventExtensionName); + return input; + } + } + public class CSharpTestExecutorIntParam: AbstractSqlServerExtensionExecutor { public override DataFrame Execute(DataFrame input, Dictionary sqlParams){ diff --git a/language-extensions/dotnet-core-CSharp/test/src/native/CSharpExecuteTests.cpp b/language-extensions/dotnet-core-CSharp/test/src/native/CSharpExecuteTests.cpp index 0d48b92b..9a61b59d 100644 --- a/language-extensions/dotnet-core-CSharp/test/src/native/CSharpExecuteTests.cpp +++ b/language-extensions/dotnet-core-CSharp/test/src/native/CSharpExecuteTests.cpp @@ -547,6 +547,56 @@ namespace ExtensionApiTest EXPECT_EQ(tagged->traceLevel, static_cast(Extension_Information)); } + //---------------------------------------------------------------------------------------------- + // Name: ExecuteForwardsNamedExtensionLogEvent + // + // Description: + // Drives an executor that logs through the ExtensionEventLogger.Log extensionName + // overload and verifies the forwarded XEvent carries the caller-supplied extension + // name rather than the extension's default. Covers the SDK facade path an in-process + // library (e.g. MssqlAI) uses to attribute its own activity events. + // + TEST_F(CSharpExtensionApiTests, ExecuteForwardsNamedExtensionLogEvent) + { + ASSERT_EQ(RegisterTestLogXEventCallback(sm_libHandle), SQL_SUCCESS); + + const string namedExecutor = + "Microsoft.SqlServer.CSharpExtensionTest.CSharpTestExecutorLogNamedExtension"; + const string namedMessage = "CSharpTestExecutorLogNamedExtension emitted event"; + const string expectedExtensionName = "TestExtension"; + + string scriptString = m_UserLibName + m_Separator + namedExecutor; + InitializeSession(0, 0, scriptString); + + g_capturedLogEvents.clear(); + + SQLUSMALLINT outputSchemaColumnsNumber = 0; + SQLRETURN result = (*sm_executeFuncPtr)( + *m_sessionId, + m_taskId, + 0, // rowsNumber + nullptr, // dataSet + nullptr, // strLenOrInd + &outputSchemaColumnsNumber); + ASSERT_EQ(result, SQL_SUCCESS); + + const CapturedLogEvent *named = nullptr; + for (const CapturedLogEvent &ev : g_capturedLogEvents) + { + if (ev.message.find(namedMessage) != string::npos) + { + named = &ev; + break; + } + } + + ASSERT_NE(named, nullptr) + << "Named-extension ExtensionEventLogger event was not forwarded to the host callback"; + EXPECT_EQ(named->extensionName, expectedExtensionName) + << "Forwarded event did not carry the caller-supplied extension name"; + EXPECT_EQ(named->traceLevel, static_cast(Extension_Information)); + } + //---------------------------------------------------------------------------------------------- // Name: ExecuteIsolatesSessionTaggingBetweenSessions // From 55ab583f3c3cbdafd9577eab3f71c29abb3e3e09 Mon Sep 17 00:00:00 2001 From: Justin M Date: Wed, 29 Jul 2026 11:41:04 -0700 Subject: [PATCH 9/9] Forward only the host callback extent actually copied SetHostCallbacks copies SQLEXTENSION_HOST_CALLBACKS field by field, then reported sizeof(g_hostCallbacksCopy) as the forwarded SizeInBytes. That over-claims when the host supplies a smaller structure than this build defines: the managed layer was told trailing fields were valid when the host never supplied them. Forward the host extent capped at the copy instead, so SizeInBytes never covers more than was populated. Add a static_assert tripwire over the structure layout. The field-by-field copy drops a newly added callback silently, leaving a build that succeeds and never forwards it, which is the same failure shape as an unbumped package pin. Fail the build instead, whether the new callback claims a Reserved slot or is appended. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../src/native/nativecsharpextension.cpp | 23 ++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/language-extensions/dotnet-core-CSharp/src/native/nativecsharpextension.cpp b/language-extensions/dotnet-core-CSharp/src/native/nativecsharpextension.cpp index cdfdd0d6..1e0df25d 100644 --- a/language-extensions/dotnet-core-CSharp/src/native/nativecsharpextension.cpp +++ b/language-extensions/dotnet-core-CSharp/src/native/nativecsharpextension.cpp @@ -564,6 +564,9 @@ SQLRETURN SetHostCallbacks( return SQL_ERROR; } + // Smallest structure this Extension can consume: everything through LogXEvent, + // the last field it reads. + // const SQLUINTEGER minHostCallbacksSize = static_cast(offsetof(SQLEXTENSION_HOST_CALLBACKS, LogXEvent) + sizeof(hostCallbacks->LogXEvent)); if (hostCallbacks->SizeInBytes < minHostCallbacksSize) @@ -572,10 +575,28 @@ SQLRETURN SetHostCallbacks( return SQL_ERROR; } + // The copy below is field-by-field, so a callback added to SQLEXTENSION_HOST_CALLBACKS + // is dropped silently: the Extension still builds and simply never forwards it. Fail + // the build instead, whether that callback claims a Reserved slot or is appended. + // + static_assert( + sizeof(SQLEXTENSION_HOST_CALLBACKS::Reserved1) == sizeof(void *) && + sizeof(SQLEXTENSION_HOST_CALLBACKS::Reserved2) == sizeof(void *) && + sizeof(SQLEXTENSION_HOST_CALLBACKS) == + offsetof(SQLEXTENSION_HOST_CALLBACKS, Reserved2) + sizeof(void *), + "SQLEXTENSION_HOST_CALLBACKS layout changed: copy the new callback in " + "SetHostCallbacks and cover it in the forwarded SizeInBytes below."); + + // Cap the forwarded extent at what this build can hold. Reporting + // sizeof(g_hostCallbacksCopy) unconditionally would tell the managed layer that + // trailing fields a smaller, older host never supplied are valid. + // g_hostCallbacksCopy = {}; g_hostCallbacksCopy.Version = hostCallbacks->Version; g_hostCallbacksCopy.Reserved0 = hostCallbacks->Reserved0; - g_hostCallbacksCopy.SizeInBytes = sizeof(g_hostCallbacksCopy); + g_hostCallbacksCopy.SizeInBytes = hostCallbacks->SizeInBytes < sizeof(g_hostCallbacksCopy) + ? hostCallbacks->SizeInBytes + : static_cast(sizeof(g_hostCallbacksCopy)); g_hostCallbacksCopy.LogXEvent = hostCallbacks->LogXEvent; g_hostCallbacks = &g_hostCallbacksCopy;