Thursday, July 26, 2012

Testing Case-Sensitive Behavior

In this post I want to talk about case sensitivity and how to test such behavior for file systems and file system filters. I've addressed various case-sensitive file system filter specific topics in my previous posts on FILE_OBJECT Names in IRP_MJ_CREATE and on Names in Minifilters - Implementing Name Provider Callbacks.
This isn't a particular complicated subject but a developer must be careful in their implementation to support this concept. Other than figuring out the case sensitivity of the name in the IRP_MJ_CREATE path and in the name provider paths, a developer must in general pay attention to their implementation so that if, for example, they compare names or file extensions and they would use RtlEqualUnicodeString or RtlCompareUnicodeString then they must use the appropriate value for the CaseInSensitive parameter, deduced from the IRP_MJ_CREATE parameters or from the FILE_OBJECT flags. Also, when dealing with UNICODE_STRINGs, one should never try to change the case by directly touching the bits in the string, and instead always use functions like RtlUpcaseUnicodeString and RtlUpcaseUnicodeChar (and their counterparts, RtlDowncaseUnicodeString and RtlDowncaseUnicodeChar).
However, for this post I don't want to focus on implementation but on how to test this. In my opinion the best place to start is the IFS Test suite, since it has a lot of tests that deal with the create path, and in particular some that test both case sensitive behavior and case preserving behavior. However, when running this test on a regular system it will most likely fail with this message:
E94.E98 :  +TEST+SEV2      : Test         :CaseSensitiveTest
Group        :OpenCreateGeneral
ExpectedNtStatus :00000000 STATUS_SUCCESS
Description  :{Msg# OpCreatG!casesen!14} Failure while creating a
              test file \casesen\NeSesac.dat. Note that in a previous
              step we created another test file. That other test
              file \casesen\nesesac.dat, differs from this one by
              the case of the name. 
Lookup Query :!casesen!14&pver=2195
This happens because by default Windows is case insensitive and so naturally the case sensitivity test fails. In order to enable case sensitive behavior for Windows all you need to do is change this registry value (it should be 1 be default so change it to 0):
HKLM\SYSTEM\CurrentControlSet\Control\Session Manager\kernel\obcaseinsensitive
BTW, this is described in Microsoft's KB 817921, even though the case deals with something else entirely.
So anyway, once you set this key make sure that the file system itself is case sensitive and then test your filter like you normally would, though it might be a good idea to add a couple of tests with files that have the same name with different cases, just to make sure you're exercising the relevant code paths. For example, if implementing an anti-virus program it might be interesting to test with a clean file with the name "foo.txt" and the EICAR file "Foo.txt" in the same folder, to make sure that not only the scanning happens on the apropriate file, but also that the other parts of the program (such as the quarantine engine or the cleaning engine) work as expected. Another good test is to then switch file names (so that "Foo.txt" is clean and "foo.txt" is the EICAR file), so that in case the product somehow depends on the sorting order in the directory that issue can be found and addressed.

Finally, this is an easy way to check that the file system is case sensitive (UPDATE: as Phil pointed out in the comments, this doesn't mean that the requests are going to be case sensitive, it simply means the file system supports it; whether this is on or off generally depends on the file system implementation and not the registry key value):

C:\Windows\system32>fsutil fsinfo volumeinfo C:
Volume Name : System
Volume Serial Number : 0x4297004a
Max Component Length : 255
File System Name : NTFS
Supports Case-sensitive filenames
Preserves Case of filenames
Supports Unicode in filenames
Preserves & Enforces ACL's
Supports file-based Compression
Supports Disk Quotas
Supports Sparse files
Supports Reparse Points
Supports Object Identifiers
Supports Encrypted File System
Supports Named Streams
Supports Transactions
Supports Hard Links
Supports Extended Attributes
Supports Open By FileID
Supports USN Journal

Thursday, July 19, 2012


I've recently realized that FLTFL_POST_OPERATION_DRAINING is not a very well understood feature. This is even though it is pretty well documented on the page for the PFLT_POST_OPERATION_CALLBACK function. So I think that in addition to the documentation page it might help to take a look at what happens under the hood.

So once a minifilter instance attaches to a volume it will start receiving IRPs and the filter will process them. There are many cases where filters need to process the results of the operation after the file system has completed it so the filter would register PostOperation callbacks and the filter would return an appropriate status to indicate that it wants a that PostOp callback called for each specific operation. These IRPs are generally called in-flight requests and in many cases the filter has data allocated associated with the operation (buffers allocated and possibly even replaced on the request, contexts that are sent from the PreOp callback to the PostOp callback, references (if the filter references a context in PreOp and dereferences it in PostOp) and so on). In case the instance needs to be detached (the minifilter is unloaded or just one instance is detached from the underlying volume) from a live volume, IO will continue to flow on the volume. One way to detach is to stop receiving new IO requests and then just wait for all the in-flight operations to be completed by the file system. Once all the in-flight operations have completed then FltMgr can pretty much free the instance because there can not be any more IO on it.

Unfortunately, this approach has a pretty big disadvantage. It is impossible to predict when the file system will complete all in-flight IRPs. There are certain operations that are very long-lived (for example, the directory change notification IRPs can be held in the file system indefinitely) which would make the time it takes to unload a filter or to detach an instance completely unpredictable. FltMgr howver is smarter than that and what it actually does is that it always tracks for each instance which operations are in-flight at any given time. So when an instance needs to be detached or a filter unloaded, FltMgr can actually know that it doesn't need to call any PostOp callbacks for that filter or that instance. However, there is still data associated with the request, buffers that have been replaced, contexts that are allocated and so on. If FltMgr simply stops calling the PostOp callbacks for all in-flight operations then the allocated data and the buffers would never be freed and the references would be leaked.

This is where FLTFL_POST_OPERATION_DRAINING comes in. When an instance is detached, FltMgr disconnects the instances from the IO path and then calls each PostOp callback for all the in-flight operations with the FLTFL_POST_OPERATION_DRAINING flag set. When a PostOp callback is called with FLTFL_POST_OPERATION_DRAINING flag the callback must not do any actual work (like checking the status of the operation or touching the FLT_CALLBACK_DATA structure at all) and instead the filter must just release any buffers or references they might have allocated. For most filters this is generally similar to what they do when the operation is unsuccessful (though there might be cases where this is different so this is more of a hint than an actual guideline to implement this feature). It is important to note that when the PostOp callback is called with the FLTFL_POST_OPERATION_DRAINING flag the actual operation might have already been completed or it might still be in the file system somewhere so the filter must not make any assumptions about the status of the actual operation.

Thursday, July 12, 2012

The Flags of FILE_OBJECTs - Part V

I wasn't really planning on adding another part to the series but there were still a couple of things I wanted to mention.
First, as I'm sure you've noticed if you looked at the actual flag values in the wdm.h file, there seems to be a flag missing, with the value 0x00800000, which would put it between FO_VOLUME_OPEN and FO_REMOTE_ORIGIN. Well, the easiest way to solve this mystery is to look at an older DDK where you'll find the missing flag:
So what is this FO_FILE_OBJECT_HAS_EXTENSION and why is it gone from pretty much everything from documentation to the !fileobj extension (as I mentioned in my previous posts, an easy way to figure out flag values is to set them into a structure and use one of extension that knows how to parse that structure to see what name it displays; in this case however the !fileobj extension ignored that flag). As far as I can tell, this flag is no longer used since Vista, but before Vista this was used to indicate that there was some private data allocated past the end of the FILE_OBJECT (see this thread on NTFSD). From the thread it appears that the IO manager used this flag to store the targeting information associated with the FILE_OBJECT, which was also used by FltMgr (see this other thread on NTFSD as well). Since Vista this functionality is implemented using the FSRTL_PER_FILEOBJECT_CONTEXT structure. I've explored the way this works in my previous post on tracking a minifilter's ActiveOpens files.
Anyway, this FILE_OBJECT extension seems to have been used to implement the same functionality that FsRtlInsertPerFileObjectContext provides today. FltMgr's designers had a goal to make sure that everything that FltMgr does can be implemented by a 3rd party filter and so since they couldn't expect 3rd party filters to modify private OS structures they had exposed the same functionality through the FSRTL_PER_FILEOBJECT_CONTEXT structure and related functions (FsRtlInitPerFileObjectContext, FsRtlInsertPerFileObjectContext, FsRtlLookupPerFileObjectContext, FsRtlRemovePerFileObjectContext). Since this is implemented in a different way today it makes sense that this flag is obsolete for now. Please do note, however, that it's entirely possible that it will be reused at some point in the future.
Another thing I wanted to do was to take a closer look at how the FO_DISALLOW_EXCLUSIVE flag can be used, since as I mentioned in my previous post in the series I wasn't sure what it does. Well, after some investigation I have some idea what it does but I'm not sure why exactly this behavior would be desired. So first I'd like to mention that this flag doesn't seem to be used anywhere in the WDK, neither FastFat nor CDFS seem to need it. I couldn't find anything about it online. Naturally I searched for both FO_DISALLOW_EXCLUSIVE and FILE_DISALLOW_EXCLUSIVE but there was really no mention about what they might do or where they might be used. However, there was one bit of information in the WDK that helped a lot: the fact that FO_DISALLOW_EXCLUSIVE is the only flag defined for FO_FLAGS_VALID_ONLY_DURING_CREATE. So I decided to take a very close look at Ntfs' IRP_MJ_CREATE processing in the hope that I might be able to figure out where this flag might be used.
So after some debugging and using my favorite disassembler, IDA, I've been able to find a couple of places where this flag might be checked. In particular the Ntfs!NtfsOpenAttribute function looked very promising since it had code that checked for that exact value, and moreover it was using the same offset into a structure that the FILE_OBJECT->Flags are at:
1: kd> dt nt!_FILE_OBJECT
   +0x000 Type             : Int2B
   +0x002 Size             : Int2B
   +0x02c Flags            : Uint4B
   +0x07c FileObjectExtension : Ptr32 Void

1: kd> u 960e7c14 L1
960e7c14 f7402c00000002  test    dword ptr [eax+2Ch],2000000h

1: kd> u 960e7ded L1
960e7ded f7402c00000002  test    dword ptr [eax+2Ch],2000000h

So I figured I had a pretty good shot at finding out how this is used by looking at when these flags are checked (the path that leads to the checks). That involved a lot of debugging and backtracking, good thing VMWare makes it so easy to retry exactly the same code path over and over again :). Anyway, this is the piece of code that turned out to be relevant:
1: kd> u Ntfs!NtfsOpenAttribute+0x3af L0x10
960e7dd3 7328            jae     Ntfs!NtfsOpenAttribute+0x3da (960e7dfd)
960e7dd5 ff7518          push    dword ptr [ebp+18h]
960e7dd8 50              push    eax
960e7dd9 53              push    ebx
960e7dda e8e9d70100      call    Ntfs!NtfsWriteCheck (961055c8)
960e7ddf 8845e7          mov     byte ptr [ebp-19h],al
960e7de2 c645e601        mov     byte ptr [ebp-1Ah],1
960e7de6 84c0            test    al,al
960e7de8 7535            jne     Ntfs!NtfsOpenAttribute+0x3fc (960e7e1f)
960e7dea 8b4618          mov     eax,dword ptr [esi+18h]
960e7ded f7402c00000002  test    dword ptr [eax+2Ch],2000000h
960e7df4 7429            je      Ntfs!NtfsOpenAttribute+0x3fc (960e7e1f)
960e7df6 a0187c0996      mov     al,byte ptr [Ntfs!NtfsStatusDebugEnabled (96097c18)]
960e7dfb 84c0            test    al,al
960e7dfd 7414            je      Ntfs!NtfsOpenAttribute+0x3f0 (960e7e13)
960e7dff 681f3a0000      push    3A1Fh

1: kd> u 960e7e13
960e7e13 c745e0220000c0  mov     dword ptr [ebp-20h],0C0000022h
960e7e1a e98f040000      jmp     Ntfs!NtfsOpenAttribute+0x88a (960e82ae)
What this piece of code checks is that if the file is not writable and this flag is set and the user wants to open the file exclusively (doesn't share READ, WRITE and DELETE; this particular check happens a bit further up in the code, not pictured here…) then the IRP_MJ_CREATE will fail with error 0xC0000022, STATUS_ACCESS_DENIED. I've been able to validate that this is the case using FileTest (during debugging I resorted to nasty tricks to get the thread to take that path I wanted, so I had to validate that this can be done by regular creates too), by creating a file with a Deny Write DACL for EVERYONE (so that Ntfs!NtfsWriteCheck would fail) and then opening it exclusively. It works fine if I don't specify FILE_DISALLOW_EXCLUSIVE but fails with STATUS_ACCESS_DENIED when I add FILE_DISALLOW_EXCLUSIVE to the options.
As I said before I'm not sure why this is useful, so if you know a scenario where this might be useful please add a comment and enlighten me :). Anyway, this is what I've been to figure out about this flag so I hope it helps...