Because embedded pointers bypass the kernel's access checks and marshaling helpers, you must perform access checks and marshaling of embedded pointers in device drivers manually before you can use them. Otherwise, you might create vulnerabilities that malicious user-mode code can exploit to perform illegal actions and compromise the entire system. Kernel-mode drivers enjoy a high level of privileges and can access system memory that user-mode code should not be able to access.
To verify that the caller process has the required access privileges, marshal the pointer, and access the buffer, you should call the CeOpenCallerBuffer function. CeOpenCallerBuffer checks access privileges based on whether the caller is running in kernel-mode or user-mode, allocates a new virtual address for the physical memory of the caller's buffer, and optionally allocates a temporary heap buffer to create a copy of the caller's buffer. Because the mapping of the physical memory involves allocating a new virtual address range inside the driver, do not forget to call CeCloseCallerBuffer when the driver has finished its processing.
Handling Buffers
Having performed implicit (parameter pointers) or explicit (embedded pointers) access checks and pointer marshaling, the device driver is ready to access the buffer. However, access to the buffer is not exclusive. While the device driver reads data from and writes data to the buffer, the caller might also read and write data concurrently, as illustrated in Figure 6-10. Security issues can arise if a device driver stores marshaled pointers in the caller's buffer. A second thread in the application could then manipulate the pointer to access a protected memory region through the driver. For this reason, drivers should always make secure copies of the pointers and buffer size values they receive from a caller and copy embedded pointers to local variables to prevent asynchronous modification.
Figure 6-10 Manipulating a marshaled pointer in a shared buffer
Never use pointers in the caller's buffer after they have been marshaled, and do not use the caller's buffer to store marshaled pointers or other variables required for driver processing. For example, copy buffer size values to local variables so that callers cannot manipulate these values to cause buffer overruns. One way to prevent asynchronous modification of a buffer by the caller is to call CeOpenCallerBuffer with the ForceDuplicate parameter set to TRUE to copy the data from the caller's buffer to a temporary heap buffer.
Synchronous Access
Synchronous memory access is synonymous with non-concurrent buffer access. The caller's thread waits until the function call returns, such as DeviceIoControl, and there are no other threads in the caller process that access the buffer while the driver performs its processing tasks. In this scenario, the device driver can use parameter pointers and embedded pointers (after a call to CeOpenCallerBuffer) without additional precautions.
The following is an example of accessing a buffer from an application synchronously. This sample source code is an excerpt from an XXX_IOControl function of a stream driver:
BOOL SMP_IOControl(DWORD hOpenContext, DWORD dwCode,
PBYTE pBufIn, DWORD dwLenIn, PBYTE pBufOut, DWORD dwLenOut,
PDWORD pdwActualOut) {
BYTE *lpBuff = NULL;
...
if (dwCode == IOCTL_A_WRITE_FUNCTION) {
// Check parameters
if ( pBufIn == NULL || dwLenIn != sizeof(AN_INPUT_STRUCTURE)) {
DEBUGMSG(ZONE_IOCTL, (TEXT("Bad parameters\r\n")));
return FALSE;
}
// Access input buffer
hrMemAccessVal = CeOpenCallerBuffer((PVOID) &lpBuff,
(PVOID) pBufIn, dwLenIn, ARG_I_PTR, FALSE);
// Check hrMemAccessVal value
// Access the pBufIn through lpBuff
...
// Close the buffer when it is no longer needed
CeCloseCallerBuffer((PVOID)lpBuff, (PVOID)pBufOut,
dwLenOut, ARG_I_PTR);
}
...
}
Asynchronous Access
Asynchronous buffer access assumes that multiple caller and driver threads access the buffer sequentially or concurrently. Both scenarios present challenges. In the sequential access scenario, the caller thread might exit before the driver thread has finished its processing. By calling the marshaling helper function CeAllocAsynchronousBuffer, you must re-marshal the buffer after it was marshaled by CeOpenCallerBuffer to ensure in the driver that the buffer remains available even if the caller's address space is unavailable. Do not forget to call CeFreeAsynchronousBuffer after the driver has finished its processing.
To ensure that your device driver works in kernel and user mode, use the following approach to support asynchronous buffer access:
■ Pointer parameters Pass pointer parameters as scalar DWORD values and then call CeOpenCallerBuffer and CeAllocAsynchronousBuffer to perform access checks and marshaling. Note that you cannot call CeAllocAsynchronousBuffer on a pointer parameter in user-mode code or perform asynchronous write-back of O_PTR or IO_PTR values.
■ Embedded pointers Pass embedded pointers to CeOpenCallerBuffer and CeAllocAsynchronousBuffer to perform access checks and marshaling.
To address the second scenario of concurrent access, you must create a secure copy of the buffer after marshaling, as mentioned earlier. Calling CeOpenCallerBuffer with the ForceDuplicate parameter set to TRUE and CeCloseCallerBuffer is one option. Another is to call CeAllocDuplicateBuffer and CeFreeDuplicateBuffer for buffers referenced by parameter pointers. You can also copy a pointer or buffer into a stack variable or allocate heap memory by using VirtualAlloc and then use memcpy to copy the caller's buffer. Keep in mind that if you do not create a secure copy, you're leaving in a vulnerability that a malicious application could use to take control of the system.
Exception Handling
Another important aspect that should not be ignored in asynchronous buffer access scenarios revolves around the possibility that embedded pointers might not point to valid memory addresses. For example, an application can pass a pointer to a driver that refers to an unallocated or reserved memory region, or it could asynchronously free the buffer. To ensure a reliable system and prevent memory leaks, you should enclose buffer-access code in a __try frame and any cleanup code to free memory allocations in a __finally block or an exception handler. For more information about exception handling, see Chapter 3, "Performing System Programming."
Lesson Summary
Windows Embedded CE 6.0 facilitates inter-process communication between applications and device drivers through kernel features and marshaling helper functions that hide most of the complexities from driver developers. For parameter pointers, the kernel performs all checks and pointer marshaling automatically. Only embedded pointers require extra care because the kernel cannot evaluate the content of application buffers passed to a driver. Validating and marshaling an embedded pointer in a synchronous access scenario involves a straightforward call to CeOpenCallerBuffer. Asynchronous access scenarios, however, require an additional call to CeAllocAsynchronousBuffer to marshal the pointer one more time. To ensure that your driver does not introduce system vulnerabilities, make sure you handle buffers correctly, create a secure copy of the buffer content so that callers cannot manipulate the values, and do not use pointers or buffer size values in the caller's buffer after they have been marshaled. Never store marshaled pointers or other variables required for driver processing in the caller's buffer.