Common Driver Reliability Issues - 2
Windows Platform Design Notes
Design Information for the Microsoft® Windows® Family of Operating Systems
Common Driver Reliability Issues
Abstract
This paper provides information about writing drivers for the Microsoft® Windows® family of operating systems. It describes a number of common errors and suggests how driver writers can find, correct, and prevent such errors.
Contents
Introduction 3
User-Mode Addresses in Kernel-Mode Code 3
Probing 4
Addresses Passed in METHOD_NEITHER IOCTLs and FSCTLs 6
Pointers Embedded in Buffered I/O Requests 7
Using Handles in User Context 8
Driver I/O Methods and Their Tradeoffs 9
Buffered I/O 9
Direct I/O 13
Neither Buffered nor Direct I/O (METHOD_NEITHER) 15
Device State Validation 15
Cleanup and Close Routines 16
Device Control Routines 16
Synchronization 17
Shared Access 18
Locks and Disabling APCs 19
Handle Validation 21
Requests to Create and Open Files and Devices 23
Opening Files in the Device Namespace 23
Long File Names 24
Unexpected I/O Requests 25
Relative Open Requests for Direct Device Open Handles 25
Extended Attributes 26
Driver Unload Routines 27
Work Items 27
Driver-Created Threads 28
Timers 28
Queued DPCs 29
IoCompletion Routines 29
Pageable Drivers and DPCs 29
User-Mode APIs 29
NtReadFile and NtWriteFile 29
TransmitFile 30
StartIo Recursion 30
Passing and Completing IRPs 30
Copying Stack Locations Incorrectly 30
Returning Incorrect Status for an IRP That the Driver Does Not Handle 31
Losing IRPs or Completing Them More Than Once 31
Returning Incorrect Status from an IRP That the Driver Issues 31
Odd-length Unicode Buffers 31
Pool Allocation in Low Memory 32
Call to Action and Resources 32
The information contained in this document represents the current view of Microsoft Corporation on the issues discussed as of the date of publication. Because Microsoft must respond to changing market conditions, it should not be interpreted to be a commitment on the part of Microsoft, and Microsoft cannot guarantee the accuracy of any information presented after the date of publication.
This White Paper is for informational purposes only. MICROSOFT MAKES NO WARRANTIES, EXPRESS, IMPLIED OR STATUTORY, AS TO THE INFORMATION IN THIS DOCUMENT.
Complying with all applicable copyright laws is the responsibility of the user. Without limiting the rights under copyright, no part of this document may be reproduced, stored in or introduced into a retrieval system, or transmitted in any form or by any means (electronic, mechanical, photocopying, recording, or otherwise), or for any purpose, without the express written permission of Microsoft Corporation.
Microsoft may have patents, patent applications, trademarks, copyrights, or other intellectual property rights covering subject matter in this document. Except as expressly provided in any written license agreement from Microsoft, the furnishing of this document does not give you any license to these patents, trademarks, copyrights, or other intellectual property.
© 2003 Microsoft Corporation. All rights reserved.
Microsoft, Windows, Windows NT, and Win32 are trademarks or registered trademarks of Microsoft Corporation in the United States and/or other countries.
The names of actual companies and products mentioned herein may be the trademarks of their respective owners.
Introduction
Drivers occupy a significant portion of the total code base executed in kernel mode. Consequently, efforts to improve the reliability and security of the system must address this large code base.
This paper describes a variety of problems commonly seen in drivers, often with code that shows typical errors and how to fix them. The code has been edited for brevity.
This paper is for developers who are writing kernel-mode drivers. The information in this paper applies for the Microsoft Windows family of operating systems.
User-Mode Addresses in Kernel-Mode Code
When providing services to user-mode code, drivers and other kernel-mode components usually receive and return data in buffers. To avoid corruption of data, disclosure of sensitive or security-critical data, or exceptions that cannot be handled by the try/except mechanism, kernel components must ensure that each data pointer they receive from user mode is a valid user-mode pointer. This operation is called probing.
Drivers must obey the following rules when handing pointers obtained from user mode:
· Probe all user-mode pointers before referencing them.
To probe a pointer, use the macro ProbeForRead or ProbeForWrite or the memory management routine MmProbeAndLockPages.
· Enclose all references to user-mode pointers in try/except blocks. The mapping of user-mode memory can change at any instant for various reasons, such as address space deletion, protection change, or decommit. Therefore, any reference to a user-mode pointer could raise an exception.
· Assume that user-mode pointers can be aligned on any boundary.
· Be prepared for changes to the contents of user-mode memory at any time; another user-mode thread in the same process might change it. Drivers must not use user-mode buffers as temporary storage or expect the results of double fetches to yield the same results the second time.
· Validate all data received from user-mode code.
Handling user-mode pointers incorrectly can result in the following:
· Crashes caused by references to portions of the kernel address space that the Memory Manager considers reserved. It is a serious error for any driver to reference such address space.
· Crashes caused by references to I/O space, if the architecture uses memory-mapped device registers. Such references (reads and writes) can also have negative effects on the device itself.
· Disclosure of sensitive data if the caller passes a pointer to an area that is unreadable by user mode, then observes the driver’s responses or return values to deduce the contents of the protected location.
· Corruption of kernel data structures by writing to arbitrary kernel addresses, which can cause crashes or compromise security.
Probing
To understand when probing is necessary, consider the following sample routines, SetUserData and GetUserData. These samples represent fictitious system service routines, but could also be driver routines keyed on IOCTL or FSCTL values; the only difference is that the driver code is more complicated. These routines show a situation in which probing is necessary. To simplify the example, the sample routines do not include locks to prevent race conditions and similar details that normally should be present in such code.
Example Routines That Do Not Use Probing
SetUserData receives a buffer from user mode and copies it to a global location. This routine represents any kernel component that receives data from user mode.
VOID
SetUserData (
IN PWCHAR DataPtr, // Pointer from user mode
IN ULONG DataLength
)
{
//
// Truncate data if it’s too big.
//
if (DataLength > MAX_DATA_LENGTH) {
DataLength = MAX_DATA_LENGTH;
}
//
// Copy user buffer to global location.
//
memcpy (InternalStructure->UserData, DataPtr, DataLength);
InternalStructure->UserDataLength = DataLength;
}
GetUserData returns to the caller data that was previously set with SetUserData:
ULONG
GetUserData (
IN PWCHAR DataPtr, // Pointer from user mode
IN ULONG DataLength
)
{
//
// Truncate data if it’s too big.
//
if (DataLength > InternalStructure->UserDataLength) {
DataLength = InternalStructure->UserDataLength;
}
memcpy (DataPtr, InternalStructure->UserData, DataLength);
return DataLength;
}
Problems Caused by Failing to Probe
In the examples in the previous section, both SetUserData and GetUserData fail to validate DataPtr. If the pointer is invalid, the caller could cause a system crash, thus compromising operating system integrity. If the pointer specifies a memory address that the caller does not have the right to read, the caller might also be able to deduce the contents of that address. Because the operating system maintains data for all processes in global pool addresses, a caller could pass an invalid pointer and then inspect the returned data for passwords or program output text strings generated by operating system users.
Routines that have pointer validation problems like these could easily be used to compromise system security. A hostile program could repeatedly call SetUserData with kernel addresses followed by calls to GetUserData to retrieve the contents of the kernel address space. The program could then look for interesting data private to other users of the system, such as cached file data for files to which the caller has no access. In this situation, the kernel returns data that the caller has no permission to see.
In addition, reading certain kernel addresses can cause unwanted side effects. For example, some addresses are pageable but should be paged only within certain process contexts, such as thread stacks; in other contexts, a bug check can occur. Also, certain device registers may be mapped into virtual memory. Reading from memory locations that are mapped this way directly affects the hardware. For example, reading from a control register of a programmed I/O device might cause the device to lose incoming data or might start or stop the device.
Example Routines That Use Probing
Both SetUserData and GetUserData must validate every user-mode pointer.. The following shows correct code for SetUserData, which probes user-mode pointers before accessing them:
VOID
SetUserData (
IN PWCHAR DataPtr, // Pointer from user mode
IN ULONG DataLength
)
{
//
// Truncate data if it’s too big.
//
if (DataLength > MAX_DATA_LENGTH) {
DataLength = MAX_DATA_LENGTH;
}
//
// Copy user buffer to global location.
//
try {
ProbeForRead( DataPtr,
DataLength,
TYPE_ALIGNMENT( WCHAR ));
memcpy (InternalStructure->UserData,
DataPtr, DataLength);
InternalStructure->UserDataLength = DataLength;
} except( EXCEPTION_EXECUTE_HANDLER ) {
// Use GetExceptionCode() to return an error to the
// caller.
}
}
The correct code validates the pointer at DataPtr by calling the macro ProbeForRead in a try/except block.
The following shows the corrected code for GetUserData:
VOID
GetUserData (
IN PWCHAR DataPtr, // Pointer from user mode
IN ULONG DataLength
)
{
//
// Truncate data if it’s too big.
//
if (DataLength > InternalStructure->UserDataLength) {
DataLength = InternalStructure->UserDataLength;
}
try {
ProbeForWrite( DataPtr,
DataLength,
TYPE_ALIGNMENT( WCHAR ));
memcpy (DataPtr, InternalStructure->UserData,
DataLength);
InternalStructure->UserDataLength = DataLength;
} except( EXCEPTION_EXECUTE_HANDLER ){
// Use GetExceptionCode() to return an error to the
// caller.
DataLength=0;
}
return DataLength;
}
The correct code validates the pointer at DataPtr by calling the macro ProbeForWrite in a try/except block.
Addresses Passed in METHOD_NEITHER IOCTLs and FSCTLs
The I/O Manager does not validate user-mode addresses passed in METHOD_NEITHER IOCTLs and FSCTLs. To ensure that such addresses are valid, the driver must use the ProbeForRead and ProbeForWrite macros, enclosing all buffer references in try/except blocks.
In the following example, the driver does not validate the address passed in the Type3InputBuffer.
case IOCTL_GET_HANDLER: {
PULONG EntryPoint;
EntryPoint =
IrpSp->Parameters.DeviceIoControl.Type3InputBuffer;
*EntryPoint = (ULONG) DriverEntryPoint;
The following code correctly validates the address and avoids this problem:
case IOCTL_GET_HANDLER: {
PULONG_PTR EntryPoint;
EntryPoint =
IrpSp->Parameters.DeviceIoControl.Type3InputBuffer;
try {
if (Irp->RequestorMode != KernelMode) {
ProbeForWrite(EntryPoint,
sizeof( ULONG_PTR ),
TYPE_ALIGNMENT( ULONG_PTR ));
}
*EntryPoint = (ULONG_PTR)DriverEntryPoint;
} except( EXCEPTION_EXECUTE_HANDLER ) {
…
Note also that the correct code casts DriverEntryPoint to a ULONG_PTR, instead of a ULONG. This change allows for use of this code in a 64-bit Windows environment.
Pointers Embedded in Buffered I/O Requests
Drivers must similarly validate pointers that are embedded in buffered I/O requests. In the following example, the structure member at arg is an embedded pointer:
struct ret_buf {
void *arg; // Pointer embedded in request
int rval;
};
pBuf = Irp->AssociatedIrp.SystemBuffer;
...
arg = pBuf->arg; // Fetch the embedded pointer
...
// If the pointer is invalid,
// this statement can corrupt the system.
RtlMoveMemory(arg, &info, sizeof(info));
In this example, the driver should validate the embedded pointer by using the ProbeXxx macros enclosed in a try/except block in the same way as for the METHOD_NEITHER IOCTLs described in the preceding section. Although embedding a pointer allows a driver to return extra information, a driver can more efficiently achieve the same result by using a relative offset or a variable length buffer.
Using Handles in User Context
Drivers often manipulate objects using handles, which can come from user mode or kernel mode. If the driver is running in system context, it can safely create and use handles because all threads within the system process are trusted. When running in user context, however, a driver must use handles with care.
Drivers should not create or pass handles to ZwXxx routines. These functions translate to calls to user-mode system services. Another thread in the process can change such handles at any instant. Using or creating handles within a user's process makes the driver vulnerable to problems, as the following example shows.
status = IoCreateFile(&handle,
DesiredAccess,
&objectAttributes,
&ioStatusBlock,
NULL,
FILE_ATTRIBUTE_NORMAL,
FILE_SHARE_READ,
FILE_OPEN,
0,
NULL,
0,
CreateFileTypeNone,
NULL,
IO_NO_PARAMETER_CHECKING);
if ( NT_SUCCESS(status) ) {
status = ObReferenceObjectByHandle(handle,
0,
NULL,
KernelMode,
&ccb->FileObject,
&handleInformation);
By the time ObReferenceObjectByHandle is called, the value of handle might have changed if:
· Another thread closed and reopened the handle.
· Another thread suspended the first thread and then successively created objects until it received the same handle value back again.
Similarly, handles received from user mode in other ways—for example, in a buffered I/O request—should not be passed to ZwXxx routines. Doing so makes a second transition into the kernel. When the ZwXxx routine runs, the previous processor mode is kernel; all access checks (even those against granted access masks of handles) are disabled. If a caller passes in a read-only handle to a file it lacks permission to write, and the driver then calls ZwWriteFile with the handle, the write will succeed. Similarly, calls to ZwCreateFile or ZwOpenFile with file names provided to the driver will successfully create or open files that should be denied to the caller.
Drivers can use the OBJ_FORCE_ACCESS_CHECK and OBJ_KERNEL_HANDLE flags in the OBJECT_ATTRIBUTES structure to safely use handles to manipulate objects. To set these flags, a driver calls InitializeObjectAttributes with the handle before creating the object.
The OBJ_FORCE_ACCESS_CHECK flag causes the system to perform all access checks on the object being opened. Handles created with OBJ_KERNEL_HANDLE can be accessed only in kernel mode. However, drivers should use kernel-mode handles only when necessary. Use of such handles can affect system performance, because Object Manager calls that use kernel handles attach to the system process. In addition, quota charges are made against the system process, and not against the original caller.