Geoff Chappell, Software Analyst
This function is the central control point for Event Tracing For Windows (ETW). It supports many user-mode API functions for managing tracing sessions. Even the private tracing sessions that are implemented mostly in user mode need some support from the kernel and get it from this function.
NTSTATUS NtTraceControl ( ULONG FunctionCode, PVOID InBuffer, ULONG InBufferLen, PVOID OutBuffer, ULONG OutBufferLen, ULONG *ReturnSize);
The FunctionCode argument selects from the function’s many operations.
The optional InBuffer and InBufferLen arguments are respectively the address and size (in bytes) of a buffer that provides the function with input. What the function interprets of this input depends on the FunctionCode.
The optional OutBuffer and OutBufferLen arguments are respectively the address and size (in bytes) of a buffer that receives the function’s output. What the function puts into this buffer depends on the FunctionCode.
The required ReturnSize argument is the address of a variable whose value on output tells how many bytes the successful function has put into the output buffer or may tell how much the failed function might have put into the output buffer (had it been large enough).
On failure, the function returns a negative error code. The usual return for success is STATUS_SUCCESS. For a few function codes, the function can return some other (positive) indication of success, such as STATUS_MORE_ENTRIES.
Both the NtTraceControl and ZwTraceControl functions are exported by name from NTDLL in version 6.0 and higher. There, in user mode, the functions are aliases for a stub that transfers execution to the NtTraceControl implementation in kernel mode such that the execution is recognised as originating in user mode.
This NtTraceControl implementation is exported by name from the kernel in version 6.0 and higher. Only in version 10.0 and higher does the kernel also export a ZwTraceControl. The kernel-mode version of ZwTraceControl is also a stub that transfers execution to the NtTraceControl implementation but such that the execution is recognised as originating in kernel mode.
Though no NtTraceControl exists before version 6.0, the kernel in versions 5.1 and 5.2 does export functions that are recognisable as precursors in the sense that each does the work that is later done through an NtTraceControl function code. These earlier functions are WmiStartTrace, WmiStopTrace, WmiQueryTrace, WmiUpdateTrace and WmiFlushTrace. This and other functionality of NtTraceControl is also supported in versions 5.0 to 5.2 as Device I/O Control through the WMI support device. Except for this paragraph to note that NtTraceControl did not arrive out of the blue for Windows Vista, none of this earlier support in any form is any concern here.
Though the NtTraceControl and ZwTraceControl functions are not documented under either name, C-language declarations have been published by Microsoft in headers from the Enterprise edition of the Windows Driver Kit (WDK) for Windows 10 version 1511: NtTraceControl in NTWMI.H and ZwTraceControl in ZWAPI.H. Thus are Microsoft’s names and types known for the declaration above.
The following implementation notes are from inspection of the kernel from the original release of Windows 10 only. They may some day get revised to account for other versions, whether to update or to follow through with the history. Meanwhile, where anything is added about earlier versions, take it not as an attempt at comprehensiveness but as a bonus from my being unable to resist a trip down memory lane.
If executing for a user-mode request, the function has some general defensiveness about addresses passed as arguments. Failure at any of these defences is failure for the function, which typically returns STATUS_DATATYPE_MISALIGNMENT or STATUS_ACCESS_VIOLATION (showing in kernel mode as raised but handled exceptions).
The InBuffer argument can be NULL to provide no input, in which case InBufferLen is ignored (literally, treated as zero). If an input buffer is given, meaning here that InBuffer is not NULL and InBufferLen is not zero, then the whole of the buffer must be in user-mode address space.
The OutBuffer argument can be NULL so that no output is requested, in which case OutBufferLen is ignored (literally, treated as zero). If an output buffer is given, meaning here that OutBuffer is not NULL and OutBufferLen is not zero, then the whole buffer must be in user-mode address space and be writable (at its first byte and also for a byte at each page boundary that is inside the buffer).
A variable for learning how much output is or could be produced is required. The variable must be in user-mode address space and be writable. If instead ReturnSize is NULL, the function returns STATUS_INVALID_PARAMETER.
If executing for a kernel-mode request, all arguments are trusted as given. This means in particular that behaviour is undefined if a non-zero buffer size is given for a NULL address and there is no rejection of NULL for ReturnSize.
Except for the following function codes in the applicable versions
or if given no buffer for either input or output, the function double-buffers. Specifically, it obtains from the paged pool an allocation whose size is the larger of the input and output buffers. If it cannot get this memory, it returns STATUS_NO_MEMORY. If an input buffer is given, the function copies the whole of it to the double buffer so that all further work with the input is from the double buffer, not from the input buffer. If the function prepares output, it does so in the double buffer and copies to the caller-supplied output buffer only when about to return STATUS_SUCCESS.
The function never accesses InBuffer, OutBuffer or ReturnSize without preparing for exceptions. The occurrence of an exception during such access is fatal to the function, which returns the exception code as its own result.
Microsoft’s names for eight of the valid function codes are known from type information in symbol files that Microsoft first published for Windows 8—though even then, not the symbol files for the kernel, which interprets the codes, nor for the obvious low-level user-mode DLLs that use the codes for their calls to NtTraceControl. Instead, they somehow find their way into symbol files for such things as AppXDeploymentClient.dll.
If only as known to these user-mode modules, the function codes apparently take their values from an enumeration named ETWTRACECONTROLCODE. A formal C-language definition is published in the NTETW.H from the Enterprise WDK for Windows 10 version 1511, but repeats just the eight that had been disclosed in symbol files. This header’s inclusion by source code for some of Microsoft’s user-mode software is presumably where those few symbol files get type information for the enumeration. It is not impossible that the full enumeration is defined for the kernel from some other header and even to give it a different name.
The table below lists the function codes that the function does not dismiss as invalid (after the preceding defences). For all others, the function returns STATUS_INVALID_DEVICE_REQUEST.
|Numeric Value||Symbolic Name||Versions|
|0x01||EtwStartLoggerCode||6.0 and higher|
|0x02||EtwStopLoggerCode||6.0 and higher|
|0x03||EtwQueryLoggerCode||6.0 and higher|
|0x04||EtwUpdateLoggerCode||6.0 and higher|
|0x05||EtwFlushLoggerCode||6.0 and higher|
|0x0B||real-time connect||6.0 and higher|
|0x0C||EtwActivityIdCreate||6.0 and higher|
|0x0D||EtwWdiScenarioCode||6.0 and higher|
|0x0E||real-time disconnect consumer by handle||6.0 and higher|
|0x0F||register user-mode GUID||6.0 and higher|
|0x10||receive notification||6.0 and higher|
|0x11||send notification||6.0 and higher|
|0x12||send reply data block||6.0 and higher|
|0x13||receive reply data block||6.0 and higher|
|0x14||EtwWdiSemUpdate||6.0 and higher|
|0x15||get trace GUID list||6.0 and higher|
|0x16||get trace GUID information||6.0 and higher|
|0x17||enumerate trace GUIDs||6.0 and higher|
|0x18||register security provider||6.0 and higher|
|0x19||query reference time||6.2 and higher|
|0x1A||track provider binary||6.2 and higher|
|0x1B||add notification event||6.3 and higher|
|0x1C||update disallow list||10.0 and higher|
|0x1E||set provider traits||10.0 and higher|
|0x1F||use descriptor type||10.0 and higher|
|0x20||get trace group list||10.0 and higher|
|0x21||get trace group information||10.0 and higher|
|0x22||get disallow list||10.0 and higher|
|0x23||set compression settings||1607 and higher|
|0x24||get compression settings||1607 and higher|
|0x25||update periodic capture state||1703 and higher|
|0x26||get private session trace handle||1703 and higher|
|0x27||register private session||1703 and higher|
|0x28||query session demux object||1703 and higher|
|0x29||set provider binary tracking||1709 and higher|
|0x2A||1709 and higher|
The function’s behaviour varies greatly with the function code. Follow the links.
For each function code, the function may of course succeed or fail. If it succeeds, it may have prepared output in the double buffer. If so, it copies this output to the caller-supplied OutBuffer. With or without output, the successful function also sets the variable at ReturnSize to the number of bytes it has placed in the output buffer.
The failed function does not produce output but it may set the variable at ReturnSize to show what output it might have produced in different circumstances. The obvious such circumstance is that OutBufferLen was too small. This is indicated by the return of STATUS_BUFFER_TOO_SMALL for the following function codes:
No matter what the error code, the function sets the variable at ReturnSize if the function code is any of: