OSRLogo
OSRLogoOSRLogoOSRLogo x Seminar Ad
OSRLogo
x

Everything Windows Driver Development

x
x
x
GoToHomePage xLoginx
 
 

    Thu, 02 Oct 2014     104754 members

   Login
   Join


 
 
Contents
  About This Site
What's New?
OSR Dev Blog
The NT Insider
The Basics
File Systems
Downloads
ListServer / Forum
Driver Jobs
Store
  Express Links
  · The NT Insider Digital Edition - Sept-Oct 2014 Now Available!
  · Sept-Oct Issue of The NT Insider Released!
  · Writing WDF Drivers: Advanced Implementation Techniques
  · OSR Seminar Schedule
  · Windows 8.1 Update: VS Express Now Supported

Meandering Through the Object Manager -- How to Get From Create to a Target Device Object

One topic that we often find driver writers don't thoroughly understand is how Windows determines--from a user Create request--the target driver and Device Object. This article addresses the intricacies of this process.

The Basics
A driver writer knows that the I/O Manager creates a Driver Object any time a driver is loaded by the PnP Manager or Service Control Manager. This object is the I/O Manager's representation of the driver's code and data in kernel space, and it is the object through which driver writers export their entry points (e.g., dispatch functions, unload, startio, etc.) to the operating system.

A Device Object is a driver's external interface, a target for I/O requests. A driver creates a Device Object in order to make the driver's services available to other Windows entities via the I/O Manager. These services may be those provided by the driver from a physical device, or may be non-hardware-related services provided by the driver via a virtual device.

One of the decisions a driver writer makes is to whom he wants to make his driver's services available. To that end, a driver may create a Device Object with a name in the "Device" part of the Object Manager's namespace. We will refer to this name as the "native" device name, because it is only directly accessible (easily) from "native" Windows entities, but not from user-mode applications. Alternatively, a driver may create a Device Interface. A device interface makes the device name visible to both user-mode and kernel entities. In addition to or instead of a device interface, some drivers elect to create a symbolic link to make their services available to user-mode applications. Both the device interface and symbolic link are objects in the "\Global??" directory of the Object Manager's name space. Because these objects are located in the "\\Global??" Object Manager directory, user applications can easily access them, and thus use the services of the related driver.

What many driver writers do not know is that the Driver Object and Device Object are objects created by the Object Manager on behalf of the I/O Manager. The Object Manager's main job is to maintain the system-wide catalog of all objects in the system. Another job of the Object Manager is to parse names passed to it by the I/O Manager whenever a Create File request is received. The Object Manager takes the input name and parses its components--which are each separated by a "\"--until it finds the object that is the target of the request. Once found, the target object receives the IRP_MJ_CREATE request with a FILE_OBJECT that contains any remaining unparsed components of the name.

Names
So now it is time to discuss the two types of names used by drivers to make devices accessible: Device Name and Device Interface. Then we'll discuss some Object Manager concepts, including symbolic links.

A native device name is the name that a driver writer associates with a device object when he calls either IoCreateDevice or IoCreateDeviceSecure. While there are no hard and fast rules about how device names are formatted, the names used usually take the format "\Device\ InstanceName#," where "\Device\" indicates the directory in the Object Manager's namespace where the object name is to reside, "InstanceName" is a unique name used by the driver typically indicating the type of device (e.g., Tape, Disk, Com, etc.), and "#" is an instance number typically starting at 0. This native device name is a name that other kernel-mode components can use if they need to use the services that your driver provides. Having a native device name is a must if you want to set up a symbolic link from your device to make it accessible from Win32 applications.

The second type of name that we will talk about is the device interface. A device interface is created by calling IoRegisterDeviceInterface and specifying--among other parameters--an interface class GUID. This interface class GUID is used to uniquely identify all the devices supporting a similar interface. For example, if your device driver registered a symbolic link using the Windows defined GUID "GUID_DEVINTERFACE_COMPORT," your driver would be expected to support all the functions that a Windows serial port (COMPORT) supports. Likewise, if you registered a device interface using the GUID "GUID_ DEVINTERFACE_DISK," you would be expected to support all the functions that a Windows disk device supports. If a predefined Windows interface GUID does not meet your needs, then you can create your own unique interface GUID using the GUIDGEN utility (available in the DDK).

IoRegisterDeviceInterface creates a unique symbolic link in the "\Global??\" directory of Object Manager namespace (a symbolic link being a type of Object Manager object that translates a reference from its own name to a different object name). The Symbolic Link name that IoRegisterDeviceInterface creates is a conglomeration of the interface GUID and other instance-specific information.

Device interfaces were introduced in Windows 2000 to overcome some inherent weaknesses in symbolic link naming. Prior to Windows 2000 the only way that an application could determine how many devices of a certain type were available in the system was to call CreateFile("InterfaceName#",...), incrementing the instance number each time and repeating until the function call failed. This technique does not work on an operating system that supports PnP, since a user may randomly remove a device in the sequence. Another weakness is inherent in the naming itself. Consider this example: two toaster oven device manufacturers write a driver for their device, and each of these drivers supports the same services but use different symbolic names. A user application that wants to use these devices must know the naming conventions used by these drivers for their symbolic links. If, however, the manufacturers agreed on a standard interface GUID, then the user mode application would not have to care about which manufacturer's device was installed.

Device interfaces solve these problems by allowing an application to enumerate all the devices that have registered a particular device interface using the Win32 SetupDiXXX API. SetupDi returns the name of the symbolic links for each device instance that was registered with the indicated device interface GUID. Once enumerated, the application can use one or more of the returned symbolic link names as the name argument to CreateFile.

Object Manager Concepts
As mentioned previously, the Object Manager is responsible for maintaining information about all the objects in the system. The Object Manager utilizes three types of objects to maintain this information:

  • Object Type -- This is the object type that is used to allow other kernel-mode entities (such as integral subsystems) to define new unique object types. For example, when the I/O Manager is initialized during system boot, it calls the Object Manager and creates the object types (such as device objects, driver objects, and file objects) that the I/O subsystem will subsequently use to perform its work. The object that is used by the Object Manager to define these dynamically-defined object types is the Object Manager's object-type object. Note that each object type that is defined has a set of methods (such as Open, Close, Parse, Delete, Security, QueryName, etc.) associated with it. These methods will be called by the Object Manager when required in order to service objects of that type.
  • Directory objects -- A named object used to contain other named objects. So, directory objects are just like the directories used in file systems.
  • Symbolic Link -- An object that is used to translate a reference to one named object into another name. When created, it links the name of the symbolic link to the name of a target object. This target object could be any type of Object Manager object, including another symbolic link.

How are these Object Manager constructs used? As we said previously, when a driver creates a named device object, the driver also indicates into which Object Manager directory to put the object's name. For device objects, this directory is by convention the "\Device\" Object Manager directory. Note that there's no rule that device objects must be placed into this directory. It's just the way it's usually done.

Symbolic links are typically used by drivers to create a name for a device object that Win32 applications can conveniently use and thus gain access to the services that a driver provides. Drivers do this by creating a symbolic link in the Object Manager?s "\Global??\" directory that points to the native device name, which is--as we previously mentioned--typically in the Object Manager's "\Device\" directory. Why does creating a symbolic link in the "\Global??\" directory make the device name conveniently accessible to Win32 applications? This is simply because the Win32 subsystem defaults to searching the Object Manager's "\Global??\" directory when attempting to resolve the name passed to the CreateFile function. We'll describe this process in much more detail later in this article.

A typical symbolic link name may be anything, and it doesn't need to match or even be similar to the native Windows device name to which it points. Typically, however, symbolic link names created by drivers are in the format "InstanceName#," where "InstanceName" is a unique name that you select and "#" is an instance number, usually starting at 0. Note that the relationship of symbolic links to target objects can be many-to-one. Thus, in some instances a driver may create multiple symbolic links which reference the same target device.

Once this symbolic link is created using IoCreateSymbolicLink, a user mode application can open the device by specifying "InstanceName#" in the CreateFile call. When the user is creating the symbolic link, he must determine to whom that name is to be visible. For more information on this subject, see the sidebar entitled "Terminal Server and its Effect on Namespaces," below.?

Putting the Concepts Together
Before we start talking about how the Object Manager finds a target device, let's review the objects we talked about to this point. Figure 1 diagrams the objects which lie within different directories in the namespace and to whom they refer.

Figure 1 -- Visualizing the Object Manager Objects

Starting with the "Devices" Directory Object, you can see two contained objects named "Serial0" and "0000005C". "Serial0" is the name that the driver used when it created the Functional Device Object (FDO). "0000005C," on the other hand, is the name that the Bus Driver used when creating the Device Object which represents the Physical Device Object for the device on its bus.

Still looking at Figure 1, you can see that the "\Global??\" Directory Object contains two Symbolic Links, "Com1" and "ACPI#PNP0501#2#{86e0d1e0-8089-11d0-9ce4-08003e301f7c}" (which we'll refer to hereinafter as "ACPI#...1f7c}"). The symbolic link "Com1" refers to "\Device\Serial0"--that is, the object named "Serial0" in the Object Manager's "\Device\" directory. The other symbolic link "ACPI#PNP...1f7c}" refers to "\Device\0000005C," which is the object named "0000005C" in the Object Manager's "\Device\" directory. The serial port driver presumably created the symbolic link "Com1" by calling IoCreateSymbolicLink, which takes the Device Object's name as input. The serial port driver presumably created the symbolic link "ACPI#...1f7c}" by calling IoRegisterDeviceInterface, and passing in the GUID which represents the COMPORT interface (which happens to be {86e0d1e0-8089-11d0-9ce4-08003e301f7c}), and a pointer to the PDO.

So now that we have a conceptual view of the Object Manager's namespace, let's talk about how it's used to find the target of a request.

Parsing a name
In Win32, whenever a user issues a CreateFile request, the Win32 subsystem modifies the passed-in file name to include default Object Manager directory information. It then passes the open request--using the modified name--to the I/O Manager via the NtCreateFile native system service.

For example, if a user issues a CreateFile("COM1",...) request, the Win32 subsystem modifies the CreateFile request to be a NtCreateFile("\Global??\COM1",...) call. This change results in a call to the NtCreateFile handler in kernel mode. The NtCreateFile handler takes the input information and calls the I/O Manager's IoCreateFile API, passing in the input name "\Global??\COM1" along with about thirteen other parameters. IoCreateFile does some basic checking of the input parameters and then passes the input name, thread Security Identifier (SID), and desired access mode to the Object Manager for processing.

When the Object Manager gets the name, it begins parsing it component by component to determine which object is the target of the Create request. It must also perform full access validation as well as audit the access attempt when the target of the request is determined.

Figure 2 -- Global?? Directory Object

In our example, the first part of the input name that the Object Manager parses is "\Global??\" (by the way, if you run the program OBJDIR--available in the "Downloads" section--you can follow along). This name "Global??," as you can see in Figure 2, represents a directory in the namespace, which (like a directory on a disk) is a container of objects.

With the first item in the name parsed, the Object Manager continues parsing by looking in the "Global??" directory to search for the next name component, "COM1."

Figure 3 -- COM1 Symbolic Link Object

As we see in Figure 3, "COM1" is found within the "Global??" directory and is an object of type symbolic link. As mentioned previously, a symbolic link is used to translate a reference to one named object into a different name, which in this case translates the name "\Global??\COM1" into the name "\Device\Serial0."

When this translation occurs--because the object found was a symbolic link--the Object Manager reparses the translated name, starting with the root of the Object Manager namespace. Thus, it restarts the parsing with the first component of the new name, "Device." "Device"--like "Global??"--is a directory in the namespace. Thus, the Object Manager opens the "Device" directory and searches for the second component of the name, "Serial0."

How long does this parsing process continue? The Object Manager's search of its namespace terminates whenever it finds a leaf node; that is, something that cannot be parsed any further by the Object Manager. Because the Object Manager has reached a Device Object in our example (and not a directory or symbolic link), the Object Manager knows that with the discovery of a Device Object named "Serial0" the search of its namespace is complete.

Figure 4 -- Serial 0 Device Object

As you can see in Figure 4, "Serial0" was located in the "Device" part of the namespace and is of type "Device," which corresponds to a Device Object.

So, with a terminal object within the Object Manager's namespace located, the Object Manager knows that it has found the "target" of the user's Create request. In other words, in our example, the Object Manager has found the device that corresponds to the internal name "\Device\Serial0" and the Win32 name "Com1."

In passing, it's interesting to note that if we were using the SetupDI API to enumerate all devices using the GUID_CLASS_COMPORT GUID, we would have found an instance named "ACPI...1f7c}" which then we would have used on our CreateFile request. The parsing of this request would have led us to the Device Object named "\Device\0000005C," and as you can see in Figure 1, this Device Object is the PDO to which "\Device\Serial0" is attached.

Enter the I/O Manager
Now that the responsible Device Object has been found, is the parsing process complete? Not yet! Earlier, we said that because a leaf node in the Object Manager's namespace has been reached, the Object Manager's job was complete. However, you'll notice that a Create IRP hasn't been issued yet. So, what happens next? To continue the job of parsing the name the caller provided, the Object Manager calls the object-specific parsing method. Because Device Objects are owned by the I/O Manager, the parse method associated with Device Objects is provided by the I/O Manager. Specifically, the parse method provided by the I/O Manager for Device Objects is named IopParseDevice.

IopParseDevice continues the parsing job by creating a File Object. The File Object is the ultimate output of a CreateFile operation and represents a single open instance of a file, device, or any other possible I/O target. The File Object contains information on how the object specified in the NtCreateFile request is being opened--for example, read or write access--as well as sharing preferences and lots of other information. The File Object also contains the name of the object to be created or opened, which in our example would be the null string.

Why would the name be the null string, you ask? If you remember, the name we were parsing after we found the symbolic link was "\Device\Serial0." When we parsed its components we found that "Serial0"--the last component of the input name--represented the Device Object or target for the user's NtCreateFile request. Since there are no more name components after Serial0 (the terminal item in the Object Manager's namespace), the I/O Manager has no unparsed name components left with which to initialize the File Object's FileName field.

If the user had done a CreateFile("Com1\x\y\z",...), then the Object Manager would have parsed up through and including the name "Serial0" and initialized the File Object it creates with the name "\x\y\z." This, however, brings up an interesting issue for the driver writer: What does "\x\y\z" mean to it? Every Device Object has an associated namespace. Names in a device's namespace are paths that begin with the device's name. So in our example using COM1, the device's namespace would begin with "\Device\Serial0." This means that "\x\y\z" is to be interpreted as an open request for "x\y\z" in device Serial0's namespace. Now, most driver writers do not implement a namespace. However, the driver must still implement internal security checks to prevent unauthorized access to its device's namespace. There are two ways that devices which do not support namespaces can ensure that access to their namespace is handled correctly:

  • They can either set the FILE_DEVICE_SECURE_OPEN flag in their Device Object flags field, which treats any open request to a device's namespace as an open to the Device Object itself, or
  • They can check the length of the filename contained within File Object (passed in the IRP_MJ_CREATE request) to ensure that the length is 0, otherwise they can fail the request.

If the driver does not implement a device namespace, it is up to the driver to determine the security checks that are performed. However, it's important to note that irrespective of whether or not a device namespace is implemented, the driver must always ensure (by setting FILE_DEVICE_SECURE_OPEN or otherwise) that appropriate access checks are performed when one of its devices is opened.

With the File Object created, the I/O Manager completes the parsing job by building an IRP with major function code IRP_MJ_CREATE and sending that IRP (via IoCallDriver) to the terminal object identified by the Object Manager (which is "Serial0" in our example). Thus, the driver responsible for the "Serial0" device receives and processes the Create IRP.

Assuming the driver completes the Create IRP with success, the caller's Create request succeeds. IopParseDevice returns to the Object Manager, and the Object Manager returns a handle to the created File Object representing this open instance to the I/O Manager, which in turn returns that handle to the caller. Whew!

The Driver Does the Parsing
You might be wondering how the name (or, more correctly, the part of the name) that was passed in the File Object is used. The answer is, it's the responsibility of the driver receiving the create IRP to parse.

Using "Com1\x\y\z" as an example is really nonsense because the Serial0 Driver (like most drivers) never really looks at the name passed in on the Create request because they weren't written to do so. Other drivers like file systems would, however, expect to parse the name contained with the File Object because they implement a namespace of their own (in addition to the one provided by the Object Manager).

Figure 5 -- Y: Symbolic Link

Consider the Win32 call CreateFile("Y:\Dir1\Dir2\Fred.txt",...), where Y is a LanManager network drive. In this example (and you can see this by following along with OBJDIR) when parsing this name the Object Manager will find a Symbolic Link "Y:" in the namespace (see Figure 5). This means that for this system the Device Object corresponding to "\Device\LanmanRedirector" will receive a IRP_MJ_CREATE request with a File Object containing the name "\Z:0000000000010bf4\redball\users\cariddi\Dir1\Dir2\Fred.txt." It will be that driver's responsibility to parse and validate the components of the input name to ensure that access is valid for its namespace.

Summary
Understanding how Windows determines the target of a Create request is a hefty topic, as you can see. It illustrates the importance of understanding the process that the Object Manager goes through to connect the name that you created in the namespace to the Device Object that you created to handle requests for users of your services. Additionally, knowing your responsibilities when it comes to securing your Device Object's namespace is vital to ensuring that proper access.

Terminal Server and its Effect on Namespaces

A generally overlooked topic is how the implementation of Terminal Server effects a user's view of the Object Manager, specifically, the user's view of the \DosDevices directory (also known as the \?? directory). On a system running Terminal Sever there is one global and one local \DosDevices directory for each logon session. The global \DosDevices directory contains the names that are visible system-wide, and the local \DosDevices directory contains the names that are only visible to the logon session. Thus one user's logon view may be considerably different than another's.

How does this effect a driver writer?Well, it means that the driver writer has to decide in which namespace to place the symbolic link. If the driver writer wants to create its symbolic link in the global \DosDevices directory, it can do so by creating the symbolic link while running in the system process context, such as when the driver's DriverEntry, AddDevice, or IRP_MN_PNP\IRP_MN_START_DEVICE routine is called. Alternatively, the symbolic link can be created in the Global \DosDevices directory by specifying \DosDevices\Global as the location for the symbolic link. If you want to create the symbolic link in a certain process' view of the \DosDevices namespace, then you must create the symbolic link when running in that process' context.

You should also be aware of how the namespaces are initialized and searched on Windows 2000 and later systems, because it may affect your design. On Windows XP, when looking for a name in the \DosDevices directory, the local directory is searched first and then the global directory. On Windows 2000 and NT4.0, however, when a new logon session is created the system copies the contents of the global directory to the local directory; and the system then searches the local directory only. The downside of this is that subsequent changes to the global directory are not reflected in the session's local directory.

By the way, when you're deleting the symbolic link you created, be sure to delete it from the correct directory in the namespace.

That about sums it up. If you need more information you can check out Local and Global MS-DOS Device Names in the latest DDK.

User Comments
Rate this article and give us feedback. Do you find anything missing? Share your opinion with the community!
Post Your Comment

Post Your Comments.
Print this article.
Email this article.

Windows Internals and SW Drivers
LAB

Dulles/Sterling, VA
20-24 Oct 2014

Developing File Systems for Windows
Seattle, WA
4-7 Nov 2014

Kernel Debugging and Crash Analysis
LAB

Boston/Waltham, MA
10-14 Nov 2014

 
 

Windows Debugger

Checked Build Downloads
29-Apr-10

Debugging Symbols

WDK Documentation

Windows WDK

 
 
x
LetUsHelp
 

Need to develop a Windows file system solution?

We've got a kit for that.

Need Windows internals or kernel driver expertise?

Bring us your most challenging project - we can help!

System hangs/crashes?

We've got a special diagnostic team that's standing by.

Visit the OSR Corporate Web site for more information about how OSR can help!

 
bottom nav links