Receive Notification

When given 0x10 as its FunctionCode argument, the NtTraceControl function receives a notification. Microsoft’s name for this function code is not known, though EtwReceiveNotification or EtwReceiveNotificationCode might be reasonable guesses. This note deals only with behaviour of NtTraceControl that is specific to this function code. The function’s general behaviour is here taken as assumed knowledge.

The notification will be intended for one or all of the calling process’s user-mode registrations of an event provider. The notification comes with a data block. This begins with a fixed-size header that tells something of where the data block came from and what’s expected for its handling, but the continuation beyond the header is essentially arbitrary. The sender, typically some other process, will have sent the notification by calling NtTraceControl with 0x11 as the FunctionCode. The header may indicate that a reply is requested. The recipient replies by calling NtTraceControl with 0x12 as the FunctionCode. The sender receives the reply by calling NtTraceControl with 0x13 as the FunctionCode, thus completing an exchange.


Much of this mechanism, and certainly this function’s part in it, is unknown to event providers. Even the lowest-level and most specialised or secret event providers do not themselves call this function or any higher-level form of it. The architecture is instead that the process that hosts the event provider, and which may host more than one, has one routine that repeatedly calls this function to receive notifications for all the process’s event providers. On each receipt, this routine interprets which event provider the notification is intended for, and then distributes it to the event provider by calling the NTDLL function EtwDeliverDataBlock. The event provider learns of it through the callback function, if any, that the event provider supplied when registering. To the event provider, then, the notification is received asynchronously in some system-supplied thread.

The one and only expected caller of this function is therefore NTDLL, which implements the polling routine. Before version 6.3, NTDLL is nothing more than the carrier of this code. Executing it is instead arranged by the kernel, which locates it from its exported name EtwpNotificationThread and executes it as a thread whenever a notification is to be sent to the process but the thread is not known to be running. This is not without trouble.

Supervision of the routine’s execution moves to NTDLL in version 6.3. The routine is not now a thread by itself. Instead, a thread from the thread pool shares the polling with other work.

Parameter Validation

The caller asks only to receive a notification: no input is expected. The output buffer is to receive a fixed-size ETWP_NOTIFICATION_HEADER and some variable-size continuation. The function returns STATUS_INVALID_PARAMETER if any of the following is true:

Notifications can have been sent to a process concurrently from multiple sources with multiple event providers as their destinations. Each process has its own queue of notifications that are waiting to be received. Each such notification is held as an ETW_QUEUE_ENTRY. The queue is in fact a double-linked list whose head is in the process’s ETW_DATA_SOURCE.

The EPROCESS for the calling process must have an EtwDataSource, else the function returns STATUS_INVALID_PARAMETER. Moreover, the data source must have at least one pending notification, else the function returns STATUS_NO_MORE_ENTRIES. Such notifications are held in a queue. Each is represented by an ETW_QUEUE_ENTRY whose DataBlock points to the notification data. This data is what the caller seeks. It begins with an ETW_NOTIFICATION_HEADER whose NotificationSize is the total size, in bytes, of the data. If this is too big for the caller’s buffer, the function returns STATUS_BUFFER_TOO_SMALL having set the return size (and the caller who expects to do anything useful would better repeat the call but with a suitably bigger buffer). Otherwise, the function copies the notification data to the caller’s buffer and returns success. If the notification queue is not empty, the return is STATUS_MORE_ENTRIES (so that the caller can know to poll again rather than wait on the event).