Showing posts with label names. Show all posts
Showing posts with label names. Show all posts

Thursday, November 4, 2010

ObQueryNameString can return names with a NULL Buffer ( and an example with SR.sys)

ObQueryNameString is a very useful API. It's used in a lot of places and is a pretty good choice if you want to find the name for an OB object. However, using it is not without pitfalls. At the moment the documentation page on MSDN has this to say in the Remarks section: "If the given object is unnamed, or if the object name was not successfully acquired, ObQueryNameString sets Name.Buffer to NULL and sets Name.Length and Name.MaximumLength to zero.". What is not clearly spelled in there is the fact that the return status in this case will be STATUS_SUCCESS.

So let's recap. Any app developer can call ObQueryNameString and get STATUS_SUCCESS, but the Name.Buffer will be NULL and they might not expect that. I've seen this issue over and over again. People get a reference to an object and they query the name, get a NULL buffer and then try to read/compare/do whatever with it and they get a visit from the bugcheck fairy. Please note that since the Length and MaximumLength are both 0, people would be safe using the Rtl functions since they tend to check these sort of things..

It is interesting to understand the reasons about how people can get bitten by this as well. The documentation specifies that "If the given object is unnamed, or if the object name was not successfully acquired...", which I guess for most people it translates into "if the name was not successfully acquired then I will some error NTSTATUS… if this object is unnamed then it's not clear what I get, maybe also some error code?..". So I suppose that people that create named objects that they own (or objects that the system creates and are guaranteed to be named) imagine that they can never get the NULL buffer and STATUS_SUCCESS. But any named object can become unnamed when it is deleted. After all, the namespace entry is simply an additional reference to the object and deleting a named object simply deletes that reference, but the object might still be kept around by other references. One easy way to see this is to follow the calls to IoCreateDevice. For example, for an unnamed device one can see this :

Imediately after a IoCreateDevice for an unnamed device:

3: kd> !devobj 93602e48  
Device object (93602e48) is for:
  \FileSystem\FltMgr DriverObject 92cb6660
Current Irp 00000000 RefCount 0 Type 00000003 Flags 00000080
DevExt 93602f00 DevObjExt 93602f30 
ExtensionFlags (0x00000800)  
                             Unknown flags 0x00000800
Device queue is not busy.

3: kd> !object 93602e48  
Object: 93602e48  Type: (922d6440) Device
    ObjectHeader: 93602e30 (new version)
    HandleCount: 0  PointerCount: 1

And immediately after a named device:
2: kd> !devobj 930e0628  
Device object (930e0628) is for:
 FltMgr \FileSystem\FltMgr DriverObject 92f691e8
Current Irp 00000000 RefCount 0 Type 00000008 Flags 000000c0
Dacl 96fd2eec DevExt 00000000 DevObjExt 930e06e0 
ExtensionFlags (0x00000800)  
                             Unknown flags 0x00000800
Device queue is not busy.

2: kd> !object 930e0628
Object: 930e0628  Type: (922d7508) Device
    ObjectHeader: 930e0610 (new version)
    HandleCount: 0  PointerCount: 2
    Directory Object: 96e61948  Name: FltMgr

Please notice how the pointer count is different. Once the named device is deleted (IoDeleteDevice), the reference from the OB namespace is removed (and the object's name in the OB header is changed) and then, when the reference count eventually reaches 0, the object is freed. However, it anyone calls ObQueryNameString on one of those references, they will get the NULL Name.Buffer...

So it is perfectly possible for a driver that is working with an object that it knows it must be named to actually get in the window between when the object is removed from the OB namespace and the time the final reference is released (the driver will of course have a reference of its own in order to be able to access the object… ). So that this means is that calling ObQueryNameString might return STATUS_SUCCESS and a NULL Name.Buffer even for a named object.

I've recently had the pleasure to debug an issue with SR.sys and my virtual volume drive on XP SP3. I will share it since it was somewhat interesting and it points out to this specific issue. This is what the stack looks like:

1: kd> lm v m sr
start    end        module name
f8489000 f849af00   sr         (pdb symbols)          d:\symbols\sr.pdb\9D5432B7234C4CD2A8F6275B9D9AF41F1\sr.pdb
    Loaded symbol image file: sr.sys
    Image path: sr.sys
    Image name: sr.sys
    Timestamp:        Sun Apr 13 11:36:50 2008 (480252C2)
    CheckSum:         00012604
    ImageSize:        00011F00
    Translations:     0000.04b0 0000.04e4 0409.04b0 0409.04e4
The problem in SR is this one:
sr!SrGetObjectName+0xd4:
f849105c ff15c49b48f8    call    dword ptr [sr!_imp__ObQueryNameString (f8489bc4)]  <- call ObQueryNameString
f8491062 3bc3            cmp     eax,ebx   <- check for STATUS_SUCCESS
f8491064 894514          mov     dword ptr [ebp+14h],eax  <- save the status...
f8491067 7c24            jl      sr!SrGetObjectName+0x105 (f849108d)
f8491069 0fb707          movzx   eax,word ptr [edi]   <-  this is the Length member of the UNICODE_STRING for the name
f849106c 8b4f04          mov     ecx,dword ptr [edi+4]  <- this is the Buffer member of the UNICODE_STRING..
f849106f d1e8            shr     eax,1   <- calculate the number of characters instead of the number of bytes
f8491071 66897702        mov     word ptr [edi+2],si   <- write some value in MaximumLength… 
f8491075 66891c41        mov     word ptr [ecx+eax*2],bx  <-   write in the buffer a 0 (basically, make sure the string is NULL terminated).. But ECX can be NULL
The stack when I hit this problem looks like this:
1: kd> kbn
 # ChildEBP RetAddr  Args to Child              
00 f80b7944 f849440d 00000000 81dc0a18 e10eac08 sr!SrGetObjectName+0xed
01 f80b7990 f848ecf2 81dc0a18 8239a818 f80b79c0 sr!SrCreateAttachmentDevice+0x99
02 f80b79c4 f848ee0f 8239a818 8239a8d0 81fb4d48 sr!SrFsControlMount+0x2e
03 f80b79e0 804ef18f 8239a8d0 81fb4c90 81fb4c90 sr!SrFsControl+0x4b
04 f80b79f0 80581bc7 00000000 81dc0a18 806e6a4c nt!IopfCallDriver+0x31
05 f80b7a40 804f53d6 c000014f f80b7b00 00000000 nt!IopMountVolume+0x1b9
06 f80b7a70 80582bc0 81e1f268 81dc0a18 f80b7ba4 nt!IopCheckVpbMounted+0x5e
07 f80b7b60 805bf444 81dc0a18 00000000 81fc6600 nt!IopParseDevice+0x3d8
08 f80b7bd8 805bb9d0 00000000 f80b7c18 00000040 nt!ObpLookupObjectName+0x53c
09 f80b7c2c 80576033 00000000 00000000 00000001 nt!ObOpenObjectByName+0xea
0a f80b7ca8 805769aa 009bef80 00100001 009bef24 nt!IopCreateFile+0x407
0b f80b7d04 8057a1a9 009bef80 00100001 009bef24 nt!IoCreateFile+0x8e
0c f80b7d44 8054161c 009bef80 00100001 009bef24 nt!NtOpenFile+0x27
0d f80b7d44 7c90e4f4 009bef80 00100001 009bef24 nt!KiFastCallEntry+0xfc
0e 009beef4 7c90d58c 7c80ec86 009bef80 00100001 ntdll!KiFastSystemCallRet
0f 009beef8 7c80ec86 009bef80 00100001 009bef24 ntdll!NtOpenFile+0xc
10 009bf1f0 7c80ef87 01be31e8 00000000 01be7bf0 kernel32!FindFirstFileExW+0x1a7
11 009bf210 751b1e05 01be31e8 01be7bf0 751a2a04 kernel32!FindFirstFileW+0x16
12 009bf240 751aad1f 009bf714 00000001 000e1358 srsvc!Delnode_Recurse+0x12e
13 009bfb34 751abd1f 009bfd54 7c97b440 7c97b420 srsvc!CEventHandler::OnFirstWrite_Notification+0x3cd
14 009bff60 7c927ba5 00000000 0000006a 000e5f40 srsvc!IoCompletionCallback+0x17a
15 009bff74 7c927b7c 751abba5 00000000 0000006a ntdll!RtlpApcCallout+0x11
16 009bffb4 7c80b713 00000000 00000000 00000000 ntdll!RtlpWorkerThread+0x87
17 009bffec 00000000 7c910230 00000000 00000000 kernel32!BaseThreadStart+0x37
So as you can see, in the mount path SR.sys is trying to create their device to attach to the volume and while doing that it tries to get the name for this device:
1: kd> !devobj 81dc0a18 
Device object (81dc0a18) is for:
  \Driver\IvmVhd DriverObject 81fad590
Current Irp 00000000 RefCount 1 Type 00000007 Flags 00000050
Vpb 81ea0f10 Dacl e1f17924 DevExt 81dc0ad0 DevObjExt 81dc0c30 
ExtensionFlags (0x00000002)  DOE_DELETE_PENDING
Device queue is not busy.
This happens to be my virtual volume device, which as you can tell from the DOE_DELETE_PENDING flag, is about to be torn down. So what this all looks like is this:

1. Something is trying to open a file, see the IopCreateFile (frame 0xb)

2. Io manager, while trying to send the IRP_MJ_CREATE irp (frame 7) wants to make sure the volume is mounted. Please note that at this point the volume device is still in the OB namespace, since otherwise the ObpLookupObjectName call (frame 8) would not have been able to reference it… So at this point IO manager resolved the name to a device object and it now has a reference to the device...

3. IopCheckVpbMounted (frame 6) finds the volume is not mounted (since I dismount it before tearing it down) so it tries to mount it…

4. SR.sys gets the mount request and is trying to build a device to attach to the newly mounted volume (in case the mount succeeds). This is pretty standard stuff for a legacy filter… Anyway, in doing so it calls ObQueryNameString which no longer finds a name for the device and returns a NULL buffer. SR checks for NT_SUCCESS but doesn't check the buffer to make sure it's not null (or even the length which is 0) and it blindly tries to make sure the string is NULL terminated (which is also pointless since ObQueryNameString documentation mentions that "The object name (when present) includes a NULL-terminator and all path separators "\" in the name.")… bugcheck.

What my driver did was simply call IoDeleteDevice somewhere between frame 8 and frame 0.

I'm willing to bet that not checking for the null Name.Buffer is a pretty common mistake. For example, there is some code posted on a blog that looks like this:

Status = ObQueryNameString(FileObject->Vpb->RealDevice,OBI,Returned,&Returned ); 

if(NT_SUCCESS(Status)) { 
if(Root) { 
wcscat(OBI->Name.Buffer,L"\\"); 
OBI->Name.Length+=sizeof(WCHAR); 
} 

Monday, September 20, 2010

Namespaces (part 1) - the OB namespace

I've been getting a lot of hits to my page about name usage in file system filters so I've decided to expand on the subject of names a bit further. This blog post is more about software design (and especially about OS design) and less about file system filters.

The role of language in shaping the way we think is a very interesting subject and one I've been interested in for a while. The book "Language in Thought and Action" is a very good introduction to the subject. One of the ideas in the book is that the mapping of names to objects changes the way we think about the object. While this is true to a certain extent in programming (think about how often you heard the phrase "well, this API would have been better named BlahBlah …"), computer science as a discipline has a completely new class of problems that I'd like to focus on in this post. The problems associated with actually designing namespaces. I'm not sure why designing and identifying namespaces isn't as popular in computer science circles as other concepts like indirection and variable scope because it's at least as important.

I don't think writing a formal definition of a namespace would actually be very interesting so I'll go straight to some examples of namespaces.

Probably the best known one is the file system namespace. The main elements of this namespace are file and directory names and the namespace serves to map file paths to streams of bytes. Also quite well known is the registry and it serves a very similar purpose. For people writing kernel mode drivers in windows also a pretty familiar one is the object manager namespace (or the OB namespace), where object names are used to identify kernel objects.

In some operating systems users are used to see and work with some other namespaces grafted into the main OS namespace (in windows users don’t usually see the OB namespace, but it can be explored using tools like WinObj ). For example, the storage devices namespace, the COM ports namespace or the running processes namespace.

For developers some familiar namespaces are the types namespace and the variables namespace (in the compiler).

But there are others even more interesting. For example, a namespace doesn't have to use ASCII or UNICODE strings to identify objects. If one were to use numbers, like 1,2,3 and so on the namespace would be an array. Similar, the process handles form a namespace, where the handle is used as the name. By now it's probably pretty clear that any key-value type of structure is a namespace. Even memory is a namespace as well, where the name is the address.

Now that we have some examples of namespaces we can look at some choices the designers of these namespaces made and what is the impact of those choices on the way they are used.

First, let's look at the object manager namespace in windows (which, as I said before, I'll refer to as the OB namespace).

I'll start by listing some of the properties of this namespace. The names in the OB namespace are UNICODE strings. As is usually the case with namespaces where the names are strings, the namespace implements a hierarchy of names and it is public. Some interesting features are that it supports links from one point in the namespace to another part and that it supports objects that don't have a name (we could treat anonymous OB objects as a different namespace but that's not particularly interesting).

Support for anonymous objects is by far the choice with the biggest impact because it means that whoever implements the namespace can't use the fact that the object is removed from the namespace as an indication that the object needs to be deleted. So they must use some different technique to track object usage and in the case of OB that technique is reference counting. From a user's perspective this means they have to do the little dance that involves increasing the reference count before sharing the object with anyone and decreasing the reference count when they're done using it. It also means that removing an object from the namespace (a delete) can happen immediately on an object (as opposed to it happening when the object is closed, like in file systems). Another implication of this architecture is that it's hard keeping logs of things because an object might not always have a name, so how does one log it ? The memory address doesn't usually convey any information about the object.

The fact that a namespace supports links is also quite interesting. The designer needs to decide whether they support links to directories in the namespace or just links to "leaves" (like files). For example NTFS supports hardlinks only between files, not directories. The OB namespace however supports links to directories, which means the OB namespace can contain loops. So the designer must come up with a way to deal with potential loops in the namespace. Another interesting implication is the fact that the caller might need to remember which way they arrived at an object in the namespace (the path to that object) in a way that takes links into account. The OB namespace doesn't do that but it is required for some features (like file system symlinks) so the users of the namespace must implement that themselves.

One final characteristic is that the namespace is hierarchical. Hierarchical namespaces have some advantages from the perspective of the implementer since they allow grouping objects that belong together. The main advantages are security and support for isolation. A flat namespace on the other hand is easy to implement, but it's very limited as it is basically just a hash.

To get a better picture of the implications of implementing a hierarchical namespace versus a flat one, let's consider some namespaces that don't support hierarchies, like the named synchronization primitives namespace in windows (events, mutexes and so on). It's easy to get name collisions so each Windows application must make sure it's using a name that no one else is using. And then from a security perspective there is no way to limit listing them. Basically, you can either prevent someone from seeing any of the names or allow them to see all the names. Access control is possible, by only on a case by case basis, and there usually isn't a way to inherit security permissions from another object.

The isolation part is also pretty important. For example, consider the fact that Windows supports sessions. If helps to keep those resources that are semantically linked into a directory, so they can be easily enumerated and operated on (even if they are just links to the actual object). Isolation is really useful in virtualization because the user of that part of the namespace doesn't necessarily see all the available objects, just the ones they're supposed to see.

This is getting pretty long so I'll stop here and talk about the file system namespace in a different post. If there is enough interest I might talk about other namespaces like the processes namespace (please leave some comments if this sounds interesting to you).

Thursday, February 11, 2010

Names and file systems filters

Proper usage of names in file system filters and minifilters is a topic that comes up a lot. The reason for this is that sooner or later one has to deal with names and it is a particularly complicated area. In this post I’ll try to address some of the common problems minifilters have with names and suggest some ways that can be used to achieve some of the common scenarios.

There are a couple of factors that make names hard to work with:

1. Computer users are used to names. They think they understand file names. They might not understand many other things, but the fact that a file has a name is something that is pretty clear to everyone. This affects file system filter developers in two ways. First, all developers started as users and as such they suffer from thinking in terms of names in cases where they shouldn’t. Second, most file system filters are meant to be used by users (or written according to user specifications) and as such they need to work with names, because that’s what users know and want.

2. File systems don’t really care about names. I mean, they need names to talk to users, but file systems developers spend most of their time thinking about improving IO performance or reliability and in general about things that happen after a file is created (names matter to file systems pretty much only in the create path). File systems don’t care whether your file is a word document or not, but filters sometimes need to know and names play into this.

3. There are so many of them. You have long names and short names and then file IDs object IDs. And then you have hardlinks and symlinks. Also, there are alternate data streams that also have names. And then you have remote file systems where one needs to care about machine names and redirector names and so on. An additional problem is that users don’t usually get the finer details about all these, so specs they come up with often don’t properly address all the possible interactions.

4. The IO stack in Windows is asynchronous. Which means that a name can change at any time. Which in turn means that once a filter gets a name, it might already be useless or wrong. Sure, one can argue it’s a rare occurrence and regular users wouldn’t run into it. But what about malicious users ? You are unlikely to run into a race between renames and transactions on a regular user’s machine, but what if someone makes it happen ? A real product can’t afford to ignore such cases.

Now let’s take a look at some of the things minifilters try to do with names. It turns out that there aren’t that many. In fact, there are pretty much three types of things that minifilters do with names. Each of these classes has specific requirements which I will address at length:

1. Open files by calling FltCreateFile. This is done to scan the file or to read or write contents, to encrypt it or something like that. Once the file has been opened there usually is a handle that is used in subsequent operations so the name is not interesting anymore. Things to note here are:

  • the filter must know the name of the file at this moment. If the filter is trying to open a file that has been renamed, it needs to know the new name. Opening the file by the old name might lead to problems.
  • FltCreateFile can only be called at PASSIVE_LEVEL.

2. Send the name to user mode for some reason (to display it to the user, or to open the file in user mode, or to log operations and so on). The vast majority of these operations are not synchronous (i.e. there is no operation that is blocked in kernel mode waiting for the user to read the message). There is one common exception, which is Anti-virus software which in case it finds a virus will sometimes prompt the user for action and it needs to display the file name (it will also probably log the name, but that can be done asynchronously). The reason this is important is that by the time the name is consumed (the user reads the log for example) the name could very easily have changed. Things to note:

  • the name of the file is usually less important. If a filter logs writes, if a rename happens at the same time writes are happening, the order doesn’t usually matter much.
  • because the information is meant to consumed by the user, performance and lag in presenting the information doesn’t matter. Even in the AV case, where the user must chose some action before the kernel thread can continue, the user is much slower than the processor. So for these types of scenarios performance is not usually important (in these paths at least; overall performance impact of the product is a different issue).

3. Policy checks. This is usually done in an effort to understand if a file is interesting or not to the filter. This is usually the case where the is some policy that is enforced by the user. For example and anti-virus filter might ignore files under a certain path or an encryption filter might only encrypt .doc and .txt files. Key things here:

  • it is a bad idea to check if the file is interesting by querying and parsing the file name every single time the filters needs to know this. A better design is to cache the information about the file somewhere and then update it only when it changes. Since we are talking about name based policy here, the only place where it can change is in the rename path. Stream contexts are particularly suited for this task and what filters normally do is attach a stream context if the file is interesting. Then, when they need to decide whether the file is interesting or not they can simply get the stream context and if one is present then it is interesting.
  • The stream context is initialized at create time and is potentially changed at rename. Both these operations happen at PASSIVE_LEVEL. Some filters prefer to query the name when the operation they care about happens, but this approach usually generates more problems that it solves.

4. Virtualization. Minifilters will use names to create a virtual namespace (inject virtual files or folders into the file systems namespace or hide files). This has a different set of challenges (many information classes that expose names, directory change notifications, oplocks and so on) but querying names is fairly easy. Also, the minifilter either is the owner of part of the namespace, which means it can serialize things and it is in the position to authoritatively know what the name of the object is or it is hiding part of the namespace, which means there will be no operations on that part of it (since no one knows it’s there).

Now that we have all the pieces in place, let’s look at some of the common scenarios.

By far the most common failure is to try to get a name where it is not supported. Like at DPC or in the paging path (people want names when writes happen to a file). This has in the past made people believe that the name support in filter manager (via FltGetFileNameInformation and friends) is broken. However, that is not the case. The important thing to understand in this case is that it is almost never the case that the name is actually needed in these cases. And by needed I’m referring to how the name is going to be used. If it is a class 1 (looking at my classification above) operation (FltCreateFile) then if the name can’t be obtained FltCreateFile cannot be called anyway (by that i mean that if FltGetFileNameInformation can’t get the name then it is illegal to call FltCreateFile). For the 2nd class of operations, the approach is to queue an async work item to get the name for this file and use it (send it to user mode, log it to the file and so on). Remember that neither accuracy nor performance usually matter here so not waiting for the work item to finish is usually ok... For the 3rd class of operations it only matters if the context is not set up yet because once a context is in place the decision should be made based on it. However, the approach of getting the name the first time it is needed has some drawbacks like the fact that getting the name and setting the context can race with renames (outside of the IRP_MJ_CREATE path) so the name might become invalid immediately; also the lack of a context might mean that the file is not interesting as well as the fact that this is the first operation for a file…

 

Another common scenario is to try to open the same file the user has open. Some anti-virus filters do this to scan the files. So the minifilter gets the name of the file in pre or post IRP_MJ_CREATE and then tries to open it. This works in the sense that one can get the name both in pre and post create, but it is problematic because the name of the file can change (however AV scanners should avoid scanning in preCreate for other reasons…). It’s hard to come up with a scenario where a malicious file might end up on a user’s system by taking advantage of this, but even so it is something to consider. Another common scenario is to open an alternate data stream for a file the user has opened in a filter. The same set of issues around racing with renames applies. A solution for this is to use a rather unknown feature of the IO system, relative opens. For any ZwCreateFile or FltCreateFile when initializing the object attributes with the InitializeObjectAttributes macro there is a parameter that accepts a handle to the root directory so that a file can be opened relative to a directory. However, this can be used to solve the problems in the example above. If the name passed in to InitializeObjectAttributes is empty (the Length = MaximumLength = 0 and Buffer = NULL) then the create will open the same stream. So if a filter wants to open another file object for a stream the user has open (or an alternate data stream for the same file) then the filter can call InitializeObjectAttributes with a handle to the user’s FILE_OBJECT (one way to generate a handle is via ObOpenObjectByPointer) and use an empty name (to open exactly the same stream) or just the name of stream (to open an alternate data stream) as the ObjectName.

 

One more thing I would like to point out is that a call to FltGetFileNameInformation in preCreate might fail if the create itself will fail. So if FltGetFileNameInformation fails with a weird status in preCreate, please make sure to investigate if the user’s create would actually have succeeded. In such cases where getting the file name in preCreate is vital to the operation of the filter then the filter should most likely fail the user’s create if FltGetFileNameInformation failed. Generally it would be better it things were done in postCreate, where possible.

 

There are a lot more interesting things with names but these are some of the common things that filters try and have problems with. Feel free to ask questions about specific scenarios.