CURRENT WORK ITEM - PREVIEW ONLY

Send Notification

When given 0x11 as its FunctionCode argument, the NtTraceControl function sends a notification to some or all of an event provider’s user-mode registrations. Microsoft’s name for this function code is not known, though EtwSendNotificationCode might be a reasonable guess (EtwSendNotification being in use already as an NTDLL export). This note deals only with NtTraceControl behaviour that is specific to this function code. The function’s general behaviour is here taken as assumed knowledge.

Access

Though the sending of notifications to event providers is vital to the machinery of Event Tracing for Windows (ETW), no ordinary users of ETW involve themselves directly in this. Even the lowest-level user-mode software that might send a notification does not call NtTraceControl but instead calls the undocumented NTDLL function EtwSendNotification, which both sends the notification and receives the replies. No ordinary users of ETW involve themselves in this, either. By far the greatest use of notification is to support the enabling and disabling of trace providers, which applications arrange by calling the documented EventTraceEx2 function or its earlier forms.

Behaviour

The function’s input is a data block to send as the notification. The data block begins with a fixed-size ETW_NOTIFICATION_HEADER which describes where to send and how, but the data block’s continuation beyond this header is essentially arbitrary. The output is the same header but edited. Notably, it may tell the sender how many replies to expect and it will then provide a handle to an object through which the sender may obtain these replies by calling NtTraceControl with 0x13 as the FunctionCode, once for each reply.

The notification is sent asynchronously. The function’s successful return says only that notification is under way. Delivery depends on each recipient of the notification calling NtTraceControl with 0x10 as the FunctionCode to receive the data block. To send a reply, they call with 0x12 as the FunctionCode. The four function codes thus make a set in which each is a vital part of an elaborate inter-process exchange.

Parameter Validation

The input buffer must provide a fixed-size ETW_NOTIFICATION_HEADER followed by arbitrary other data. The output buffer is to receive exactly an ETW_NOTIFICATION_HEADER, but the caller who prepares the notification as requesting a reply will more usefully intepret this output header as an ETWP_NOTIFICATION_HEADER, i.e., in the private form which has Reserved2 explained as a ReplyHandle.

This general notion of input and output is enforced through a first tier of parameter validation. The function returns STATUS_INVALID_PARAMETER if any of the following are true:

In the particular case with EtwNotificationTypeEnable as the NotificationType, the kernel itself interprets the data block’s continuation beyond the header, and not just for the minimal 0x78 bytes. This note distinguishes the general case under the heading Notify GUID and the particular under Enable GUID.

Notify GUID

Transmission is limited to 64KB. If the NotificationSize on input exceeds 0x00010000, the function returns STATUS_INVALID_BUFFER_SIZE.

The Provider To Notify

The event provider whose registrations receive the notifications is specified in the input as the DestinationGuid. No GUID completely specifies an event provider. There are different types, represented by the ETW_GUID_TYPE enumeration. From the user-mode perspective, event providers that are registered through the documented API functions EventRegister and RegisterTraceGuids are trace providers: they have EtwTraceGuidType as their ETW_GUID_TYPE. For both these API functions, the substantial user-mode implementation in NTDLL is EtwNotificationRegister. This undocumented function can be called separately to register what might as well be called notification providers. These have EtwNotificationGuidType as their ETW_GUID_TYPE.

With one exception, this function interprets the DestinationGuid as selecting a notification provider. If the DestinationGuid is not among the notification providers, the function returns STATUS_WMI_GUID_NOT_FOUND. The function also fails if the caller does not have WMIGUID_NOTIFICATION access to the DestinationGuid.

The exception is that when the NotificationType is EtwNotificationTypePrivateLogger (4), the DestinationGuid is sought among the trace providers. Moreover, there is first a security check for whether the caller is permitted to send this notification to any trace provider. The function fails unless the caller has TRACELOG_GUID_ENABLE access to a different GUID:

GUID Symbolic Name Versions
{9E814AAD-3204-11D2-9A82-006008A86939} SystemTraceControlGuid 6.0 only
{472496CF-0DAF-4F7C-AC2E-3F8457ECC6BB} PrivateLoggerSecurityGuid 6.1 and higher

If the DestinationGuid is not among the trace providers, the function returns STATUS_WMI_GUID_NOT_FOUND. If it is, then the function also fails if the caller does not have TRACELOG_GUID_ENABLE access to the DestinationGuid.

What the notification is sent to is not the event provider but its user-mode regstrations. An event provider can be formed from any number of executable modules, in both kernel and user modes, and in any number of processes. Each registers with the kernel as contributing to this provider, but a provider can be known to the kernel (and thus avoid the preceding STATUS_WMI_GUID_NOT_FOUND error) without yet having any registrations. A notable example is that a trace provider can be (and even typically is) enabled by some tracing session in advance of the provider’s execution so that all the execution can be logged. This function needs at least one registration, else it returns STATUS_WMI_INSTANCE_NOT_FOUND.

Supporting Replies

If ReplyRequested on input is TRUE, then the function cannot usefully proceed without the means both for the target registrations to reply and for the caller to receive those replies. The means as implemented is that the function creates an ETW_REPLY_QUEUE and an ETW_REG_ENTRY.

The ETW_REPLY_QUEUE is mostly a KQUEUE. The function returns STATUS_NO_MEMORY if it fails to create this ETW_REPLY_QUEUE.

The ETW_REG_ENTRY is a specialised form of the same structure with which the kernel represents each of a provider’s registrations. For registrations by user-mode callers, the structure is an Object Manager object, specifically an EtwRegistration object. So it is too for this purpose of supporting replies to notifications. The point to being a formal object is of course that it can have a handle. Failure to create this reply object and a handle for user-mode access with the WMIGUID_NOTIFICATION and TRACELOG_REGISTER_GUIDS permissions is failure for the function.

Transmission

The event provider can have multiple registrations. As noted above, each has to execute user-mode code to receive the notification. Some or all may do so in a process other than the caller’s. The function does not wait. The data block must be capable of outliving the function. It is copied to paged pool, else the function returns STATUS_NO_MEMORY.

In general, the notification goes to all the target event provider’s user-mode registrations that are not yet closed. If TargetPID on input is non-zero, transmission is further restricted to registrations by the one process that has TargetPID as its process ID. Thus does the function determine its notifyees. There may turn out to be none, which counts as success. From here, the function can fail only if transmission fails for all notifyees. The number for which transmission at least gets under way is reported to the caller as the NotifyeeCount in the output buffer. The ReplyHandle on output is the handle to the reply object or is NULL.

If the caller requested a reply, then the NotifyeeCount on output is not only the number of registrations to which the notification is sent but is more practically the number of calls the caller should expect to make to NtTraceControl with 0x13 as the FunctionCode to receive one reply from each notifyee. The ReplyHandle will be needed as input to these calls. Perhaps as a small oversight, the ReplyHandle may be non-NULL even if the NotifyeeCount is zero, but the reply object will by then be gone and the handle will be stale.

Notification Mechanism

For a registration to receive a notification, its process must have an ETW_DATA_SOURCE. This is created (in non-paged pool) once per process and retained. Ordinarily it is created the first time that the process is the target of a notification. It is pointed to from the EtwDataSource member of the EPROCESS. If the process does not yet have one, then the registration is a failed notifyee.

A process can be sent many notifications concurrently to its many user-mode registrations of many event providers. The process’s ETW_DATA_SOURCE is the queue. An ETW_QUEUE_ENTRY is created (also in non-paged pool) for each registration that a notification is sent to. Without one, the registration is a failed notifyee. Each ETW_QUEUE_ENTRY goes into the queue for the target process and stays there until its retrieval by the target process’s call to NtTraceControl with 0x10 for the FunctionCode.

If ReplyRequested is TRUE in the data block, then the notifyee registration requires preparation since it may have to hold a reply until NtTraceControl with 0x13 for the FunctionCode is called by the notification’s sender. Although in principle the one registration might be sent any number of notifications concurrently which all request replies, the possibility is evidently thought remote in practice. Capacity is hard-coded at four. Before a notification that requests a reply can be sent, one of the four slots in the registration’s ReplySlot array must be reserved. If none are available, the registration is a failed notifyee.

WRITING IN PROGRESS

Summary

It is perhaps as well to summarise the general case’s interpretation of the header for input and output. The Input column shows each member that is meaningful on input. An empty cell means the corresponding member is not interpreted. The Output column shows each member that is set in the output buffer if the function succeeds. An empty cell means the corresponding member is unchanged.

Offset Input Output
0x00
ETW_NOTIFICATION_TYPE NotificationType;
 
0x04
ULONG NotificationSize;
 
0x08    
0x0C
BOOLEAN ReplyRequested;
 
0x10    
0x14  
ULONG NotifyeeCount;
0x18  
ULONG64 ReplyHandle;
0x20
ULONG TargetPID;
 
0x24  
ULONG SourcePID;
0x28
GUID DestinationGuid;
 
0x38    

The SourcePID on output is the caller’s process ID. It is no news to the caller. That it is set for output is here thought to be a side-effect of preparing the data block that will be seen by notifyees.

TO BE DONE

Enable GUID

When the NotificationType is EtwNotificationTypeEnable, the notification has the specific purpose that the event provider represented by the DestinationGuid is to be enabled or disabled for some tracing session. This very important but complex special case is left for another time.