Common Driver Reliability Issues

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.