This page is a placeholder for future writing of thoughts and recommendations. Treat as a work in progress.

ProbeForRead

This function tests whether a user-mode buffer is not immediately unsuitable for reading from kernel mode.

Declaration

The modern declaration appears first in WDM.H from the Windows Driver Kit (WDK) for Windows 8:

VOID 
ProbeForRead (
    VOID volatile *Address, 
    SIZE_T Length, 
    ULONG Alignment);

In the same header from the Device Driver Kit (DDK) for Windows Server 2003 through to the WDK for Windows 7, the declaration does not yet use the volatile keyword:

VOID 
ProbeForRead (
    VOID *Address, 
    SIZE_T Length, 
    ULONG Alignment);

This, however, had lost the const keyword that shows in both WDM.H and NTDDK.H from the DDKs for Windows 2000 and for Windows XP:

VOID 
ProbeForRead (
    VOID const *Address, 
    SIZE_T Length, 
    ULONG Alignment);

Parameters

The Address and Length arguments are respectively the address and size, in bytes, of what’s presented to the kernel as a readable buffer in user-mode address space.

The Alignment argument is the least power of two to which the buffer’s address must be aligned. It is perhaps as well to spell out that 1 is equivalent to there being no alignment requirement.

Return Value

Any return from the function is a success. Failure is indicated by raising an exception.

Availability

The ProbeForRead function is exported by name from the kernel in version 3.10 and then again in version 5.0 and higher. In versions 3.50 to 4.0, the kernel has the code but only as a macro or inlined routine.

Behaviour

The function fits a design in which user-mode software is never permitted any access to addresses outside some range that is constant through the whole execution of Windows. In the intended use of the function, an Address and Length are received somehow from user-mode software that proposes the Length bytes at Address as inputs to some service from kernel-mode software. Performing this service may require interpretation of the bytes, e.g., for directions about what the user-mode client asks be done. It may instead be able to treat the bytes as a black box, e.g., for mapping or copying to another user-mode address space for inter-process messaging. Either way, reading these addresses would be done on behalf of some user-mode client and must be rejected if any of the given addresses lie outside the range of address space that can ever be accessible to user-mode software.

If Length is zero, as when a buffer might be provided but is not, then since the caller has no reading of any sort to proceed to, the Address and its Alignment requirement are irrelevant and the function succeeds trivially.

Although the Address and Length are typically obtained from a user-mode client, the Alignment is just as typically the kernel-mode caller’s requirement. It may be chosen for mere plausibility or convenience, as with expecting that an array of 64-bit integers has 64-bit alignment. It may be a property that the caller regards as necessary for performing some requested service, as with page-alignment for a buffer that’s to be mapped (rather than copied) to another process’s address space. If the Address is not a whole multiple of the Alignment, then the function raises an alignment exception, i.e., with STATUS_DATATYPE_MISALIGNMENT as the exception code. Behaviour is undefined if Alignment is not a power of two.

Beyond these preparations, the essence of the function is that if any of the buffer lies in the constantly prohibited range, then the function raises an access violation, i.e., an exception with STATUS_ACCESS_VIOLATION as the exception code.

Implementation Details

The enforced range is that none of the buffer may lie at or above the address that is represented symbolically as the MM_USER_PROBE_ADDRESS. Proper use of the function does not require knowledge of what the MM_USER_PROBE_ADDRESS is—or that it has this name or even that the acceptable range is from zero up to but not including some upper bound. Still, some concreteness is helpful, if not for programming, then at least for immediate recognition when debugging.

The MM_USER_PROBE_ADDRESS macro has long been defined as the address given by the kernel’s exported MmUserProbeAddress variable. Originally, the MM_USER_PROBE_ADDRESS was defined as a constant, specifically as 0x7FFF0000. This appears to have been chosen for allowing 64KB before the start of address space that is exclusively for kernel-mode use—which was then exactly the upper half of the whole address space. Windows NT 4.0 SP3 introduced a configurable boot option for raising the start of kernel-mode address space and correspondingly allowing more for user mode, hence the introduction of MmUserProbeAddress as a variable. Still the probe address is set to 64KB below whatever the kernel’s initialisation establishes as the start of kernel-mode address space.

The x64 processor’s translation from its theoretical 64-bit address space to physical memory is in effect a 48-bit algorithm. Addresses are invalid unless the high 16 bits are sign-extended from bit 47. This creates an architectural separation of the address space into disjoint low and high regions. Windows restricts user-mode access to the low region. This runs from zero up to but not including 0x00008000`00000000. Before version 6.3, the constraint is much tighter, up to but not including 0x00000800`00000000. All x64 versions set the MmUserProbeAddress to 64KB below whatever they use as their theoretical upper bound on user-mode address space.

In assessing whether the buffer extends beyond the probe address, all versions defend against overflow. Version 3.10 computes the address of the buffer’s last byte, and rejects the buffer if either the addition has wrapped around or the last byte is not below the probe address. All later versions reject the buffer if its non-inclusive end is either above the probe address or below the start address.

There has been variation in how the function raises an access violation. Early versions raise it explicitly, originally by calling ExRaiseStatus, later by ExRaiseAccessViolation. The latter was introduced for version 3.51, plausibly to reduce all the space used by pushing the exception code in all of the kernel’s numerous inlinings of ProbeForRead and ProbeForWrite. Starting with Windows Server 2003 SP1, the function does not itself raise the exception but instead reads deliberately from the probe address, presumably expecting that this invalid access will be handled by raising the expected exception. For reasons that are not understood, what the function reads from the probe address is a dword in version 6.3 and higher but only a byte in earlier versions.

Modern implementations return, i.e., succeed, on verifying that the buffer lies wholly below the MM_USER_PROBE_ADDRESS. The original implementation, for version 3.10, instead continues such that it truly does probe the buffer. It reads one byte at the start of every page in the buffer, expecting to cause an exception, in effect as the probe’s failure, if any of these page-aligned addresses in the buffer are somehow invalid for reading. This may have been seen as redundant. After all, if the kernel-mode caller will read the buffer, e.g., for interpreting inputs, it can do so in its own good time—by when it may find that the buffer is not still readable.

IRQL

The whole point to the ProbeForRead function is that it works with addresses that may have come from user mode. Rejection certainly causes an exception, which is incompatible with execution at high IRQL. Even if the addresses are not rejected for user-mode access, the probe may in principle be implemented as reading from the address and thus cause paging. Except in version 3.10, the function is itself implemented in a paged-code section. The function cannot safely be called at DISPATCH_LEVEL or higher.

Documentation Status

The earliest that ProbeForRead is known to have been documented is the DDK for Windows 2000. The history, then, is that not until version 5.0 did Microsoft care (or realise) that non-Microsoft programmers would better have easy access to a standard way to test the plausibility of addresses received from user mode as inputs to kernel-mode services. The early history of this function, which exists only for security, may thus be useful as a data point in assessing modern perceptions of security’s early neglect.

Even with the function newly exported in Windows 2000, documentation for the function’s use by kernel-mode programmers in general was not without quirks and delays. It seems safe to say that MIcrosoft did not regard this use as very important to establish, at least not by demonstrating that Microsoft itself takes care. As late as the WDK for Windows 7, documentation still had CONST in the Address argument’s type though it had been removed from the declaration in WDM.H years before. Documentation in the Windows 8 WDK caught up on this point only to miss that the declaration had by then added volatile. As if to make up for this omission, the documentation’s presentation of Syntax was changed again, years after the original Windows 10, so that Address now has both const and volatile. This is not without grounds, but it cannot count as correctly describing the behaviour that programmers will get. Starting with the WDK for Windows 8.1, WDM.H has two declarations for ProbeForRead. The first does indeed have both const and volatile but it is specialised for use in static code analysis. The declaration that the compiler works from for generating code has only volatile.