Using FileObject and FsContext

August 29th, 2007 - Fernando Roberto

Well, with some posts and a little patience to endure my jokes, we can construct a simple driver that responds to calls from applications. A call to CreateFile function creates a connection between the application and device, returning a handle which will later be used to route reading/writting requests to the device. If there are more calls to CreateFile function, new handles will be returned. Like it happens with files, each handle has its own context. Suppose you open the same file twice so, you will get two handles with different contexts. A reading of 100 bytes from the first handle would cause the file’s current location be different from the initial position. A new reading using the same handle would result on getting the next 100 bytes ahead of the ones already read. Now if you used the second handle, you would get the first 100 bytes of the file. That’s because each handle would keep different file locations. In this post, I will talk about how to keep this context between the different opened handles for your device.

The issue in practice

Not long ago, in one of my posts, I gave a basic driver example that implements a linked list of buffers that were written to the device. The same buffers are obtained from further reads to the same device. This post will be based on this sample driver to make the tests and suggested modifications. The driver’s string list was implemented as a single global variable. If there is a single list, the various handles gotten from the applications will manipulate this same list.

Remember that the test program writes the strings, that were typed in a console at the device until an empty one is supplied. When this occurs, the application starts reading and displaying the results at the screen. Thus, the same provided string sequence should be displayed. To reproduce the problem, follow the steps below with the test driver already installed:

  1. Open up a Prompt window and execute a test program instance.
  2. Enter the string sequence that goes from “111”, “222” until “555”, but don’t enter the empty one yet.
  3. Open up a new Prompt window and execute another test program instance.
  4. On this new window, enter the string sequence that goes from “666” until “999” and then, an empty one.
  5. Go back to the previous instance and then, enter an empty string into the test program.

You should get an output as it is shown below. Note that the first test program instance has not read any string, despite having written several ones. These missing strings were got to the second test program instance.

Separating lists

Creating an algorithm able to separate these lists using the process identification could even work, but try to imagine that a single application could use two different libraries, which in turn, would use these lists. In this way, one library list would merge into the other library list, since both libraries are at the same process. To separate the context using various device openings, we use the FileObject member of the current stack location.

When an application calls the CreateFile function, this request goes to the ObjectManager which verifies the existence of the desired object, then checks whether the application has rights to obtain a handle to this object, and, if everything is agreed, the request reaches your driver in the form of an IRP with its Major Function as IRP_MJ_CREATE. To get the FileObject referring to this new connection being created, you need to do as shown at the code below.

    //-f--> Gets the FileObject referring to this list
    pStack = IoGetCurrentIrpStackLocation(pIrp);
    pFileObject = pStack->FileObject;

This or that list?

All subsequent operations that come from the same object instance will come with the same FileObject. This helps tracking the context of a certain device opening from the several ones that your driver receives.

So, all that I need to do is to create a list of FileObjects and then link each one of them to a string list? The idea of linking the FileObject to a structure that holds this context is so obvious that the system has already reserved a space into the FILE_OBJECT structure exclusively for this use.

typedef struct _FILE_OBJECT
{
    CSHORT  Type;
    CSHORT  Size;
    PDEVICE_OBJECT  DeviceObject;
    PVPB  Vpb;
    PVOID  FsContext;
    PVOID  FsContext2;
    PSECTION_OBJECT_POINTERS  SectionObjectPointer;
    PVOID  PrivateCacheMap;
    NTSTATUS  FinalStatus;
    struct _FILE_OBJECT  *RelatedFileObject;
    BOOLEAN  LockOperation;
    BOOLEAN  DeletePending;
    BOOLEAN  ReadAccess;
    BOOLEAN  WriteAccess;
    BOOLEAN  DeleteAccess;
    BOOLEAN  SharedRead;
    BOOLEAN  SharedWrite;
    BOOLEAN  SharedDelete;
    ULONG  Flags;
    UNICODE_STRING  FileName;
    LARGE_INTEGER  CurrentByteOffset;
    ULONG  Waiters;
    ULONG  Busy;
    PVOID  LastLock;
    KEVENT  Lock;
    KEVENT  Event;
    PIO_COMPLETION_CONTEXT  CompletionContext;
    KSPIN_LOCK  IrpListLock;
    LIST_ENTRY  IrpList;
    PVOID  FileObjectExtension;
 
} FILE_OBJECT, *PFILE_OBJECT;

The FsContext and FsContext2 fields are used for this purpose and unless you implement a filter, you can at ease use them. To make each device opening has its own list, it is necessary to store the head list address in one of these fields, as it is shown below at the new implementation of OnCreate. The whole source code changes are available for downloading at the end of this post.

/****
***     OnCreate
**
**      This routine is called whenever an application or driver
**      tries to get a handle to the device we have created.
*/
 
NTSTATUS
OnCreate(IN PDEVICE_OBJECT  pDeviceObj,
         IN PIRP            pIrp)
{
    NTSTATUS            nts = STATUS_SUCCESS;
    PBUFFER_LIST_HEAD   pListHead;
    PIO_STACK_LOCATION  pStack;
 
    //-f--> A hello to the debugger...
    KdPrint(("Opening EchoDevice...\n"));
 
    //-f--> Allocates the list head for this IRP_MJ_CREATE
    pListHead = (PBUFFER_LIST_HEAD) ExAllocatePoolWithTag(
        PagedPool,
        sizeof(BUFFER_LIST_HEAD),
        ECHO_TAG);
 
    if (pListHead)
    {
        //-f--> Initializing the buffer list and its Mutex
        InitializeListHead(&pListHead->BufferList);
        KeInitializeMutex(&pListHead->Mutex, 0);
 
        //-f--> We store the context to the FileObject.
        pStack = IoGetCurrentIrpStackLocation(pIrp);
        pStack->FileObject->FsContext = pListHead;
    }
    else
        nts = STATUS_INSUFFICIENT_RESOURCES;
 
    //-f--> Here we complete the IRP with success.
    pIrp->IoStatus.Status = nts;
    pIrp->IoStatus.Information = 0;
    IoCompleteRequest(pIrp, IO_NO_INCREMENT);
    return nts;
}

Is a FileObject equivalent to a Handle?

No. Initially we have a handle for each FileObject, but the number of handles to a FileObject increases as their handles are duplicated. When the CreateFile function is called, your driver receives an IRP_MJ_CREATE; however your driver is not alerted when someone calls the DuplicateHandle function. Thus, each handle points to an object but an object can be pointed out by several handles.

Who will clean this mess?

When all references to a given FileObject are released, your device will receive an IRP_MJ_CLOSE. Let’s use this event to clean up any remaining strings on the list.

/****
***     OnClose
**
**      I'll make it simple just saying that this routine is called, whenever
**      an application or driver closes the handle which was previously opened.
**      Actually, this does not happen exactly in this way. (postponed to a future post)
*/
 
NTSTATUS
OnClose(IN PDEVICE_OBJECT  pDeviceObj,
        IN PIRP            pIrp)
{
    PBUFFER_LIST_HEAD   pListHead;
    PIO_STACK_LOCATION  pStack;
    PLIST_ENTRY         pEntry;
    PBUFFER_ENTRY       pBufferEntry;
 
    //-f--> A hello to the debugger...
    KdPrint(("Closing EchoDevice...\n"));
 
    //-f--> Retrieves the list referring to this FileObject.
    pStack = IoGetCurrentIrpStackLocation(pIrp);
    pListHead = (PBUFFER_LIST_HEAD)pStack->FileObject->FsContext;
 
    //-f--> Here, we remove all the remaining nodes that were not read
    //      by the application. That would happen if the application had called
    //      WriteFile but not ReadFile.
    while(!IsListEmpty(&pListHead->BufferList))
    {
        //-f--> It takes the first node from the list.
 
        pEntry = RemoveHeadList(&pListHead->BufferList);
 
        //-f--> Retrieves the base address from the node
        pBufferEntry = CONTAINING_RECORD(pEntry, BUFFER_ENTRY, Entry);
 
        //-f--> Finally releases the memory used by
        //      that node.
        ExFreePool(pBufferEntry);
    }
 
    //-f--> Releases the memory used by the head list.
 
    ExFreePool(pListHead);
 
    //-f--> Here, we complete the IRP with success.
    pIrp->IoStatus.Status = STATUS_SUCCESS;
    pIrp->IoStatus.Information = 0;
    IoCompleteRequest(pIrp, IO_NO_INCREMENT);
    return STATUS_SUCCESS;
}

When an application is terminated or even when it falls unwillingly, all those application handles are closed by the system. Thus, it is guaranteed that your driver will always receive the IRP_MJ_CLOSE related to the object that was released. Actually, there is a little story about IRP_MJ_CLEANUP and IRP_MJ_CLOSE that we will let to the next time.

Once implemented the change, each opened handle will own list, unless the handle is duplicated. If we repeat the same sequence of the listed steps above to force the error, we will get the following output.

In a future post, I’ll show you how to give names to the lists in order to allow different processes begin able to open the same list from a known name.

CYA! 🙂

FileObjEcho.zip

Leave a Reply