Thursday, May 19, 2011

Vacation time

I'm on vacation for the next couple of weeks and i won't have the time to work on posts. See you after june 1st.

Thursday, May 12, 2011

STATUS_REPARSE and FILE_OPEN_BY_FILE_ID

Using STATUS_REPARSE is a pretty common operation in minifilters. However, I've seen a lot of minifilters misusing it in various ways. In this post I'd like to talk about how opens by ID interact with STATUS_REPARSE and how common it is to make a mistake in this path.

In one of my previous posts about the IRP_MJ_CREATE operation I mentioned that there is a structure, the _OPEN_PACKET (discussed in this post), that stores what the original IopCreateFile request looked like and I also mentioned how the IRP_MJ_CREATE IRP is populated based on that information. Of course, when STATUS_REPARSE is returned, the new IRP will have to take that into account as all as the contents of the _OPEN_PACKET. In particular the Options for the IRP come from the OPEN_PACKET while the file name comes from the buffer in FILE_OBJECT->FileName when STATUS_REPARSE was returned. In this case it's easy to make an error when the IRP_MJ_CREATE has the FILE_OPEN_BY_FILE_ID and the mini-filter blindly calls FltGetFileNameInformation() and gets a file name back. Behind the scenes FltMgr deals with the fact that the file is opened by ID, and this is transparent to the minifilter. So a minifilter might in fact be completely oblivious that the had the FILE_OPEN_BY_FILE_ID flag set and it will do what it normally does, calculate the new path based on the original file path, put it in the FILE_OBJECT->FileName and return STATUS_REPARSE. Unfortunately, when the IO manager issues the new IRP_MJ_CREATE, it will take the Options from the OPEN_PACKET (including the FILE_OPEN_BY_FILE_ID flag) and use the name supplied by the user and so the IRP_MJ_CREATE will now be broken and it will either fail or end up opening the wrong file if the new FileName is exactly of the right length and it happens to correspond to a file ID or object ID on the volume. This is rather unlikely but can happen and even worse, it can easily be exploited by an unprivileged user that only has to send a properly formatted name to NtCreateFile and trick a minifilter into opening the wrong file. Of course, the system will still perform ACL checks and so if the minifilter doesn't do break the NT security model then the user will not get access to files it doesn't already have access to, but this might be exploited to break out of a sandbox for example…

So now let's look at a code example of how this happens. There is a sample in the WDK that shows how to perform a reparse from a minifilter, SimRep. By default SimRep reparses any request for a path that starts with \x\y to a path that starts with \a\b. For my little experiment I've created two files:

  • C:\x\y\foo.txt
  • C:\a\b\foo.txt

Then before loading SimRep I used my favorite file system tool, FileTest (by the way, in my opinion this tool is definitely a must have for any windows file system and file system filter developer.) to open C:\x\y\foo.txt by ID. After I got the File ID and had the tool set to open C:\x\y\foo.txt by ID I loaded SimRep and attached it to C:. At that point trying to open the same File ID fails with STATUS_INVALID_PARAMETER (which is a result of FltGetFileNameInformation() failing). Of course, the interesting part is watching what happens in SimRep in this case. Here are some screenshots about how it all looks in FileTest:

Ok, so now that it's clear what the problem is, what can a developer do to work around this issue ? The solution is actually pretty straightforward, though it might be a bit complicated to implement… If FILE_OPEN_BY_FILE_ID is present then the minifilter must set the FileID or ObjectID of the target file instead of the name, so the minifilter might need to obtain that ID by either opening the file or the parent directory.

Thursday, May 5, 2011

IRP Completion, STATUS_PENDING and FltMgr

One of the trickiest things filter developers need to deal with is IRP completion. There are two documents that describe the issue in great detail: Handling IRPs: What Every Driver Writer Needs to Know and Secrets of the Universe Revealed! - How NT Handles I/O Completion. I tend to re-read them every time I need to deal with IO completion because the subject is pretty complex and it doesn't come up frequently enough that I am confident to get it right without a refresher. This is going to be a pretty dry post, but it is a pretty important one because it is one of the issues that I've seen that causes a lot of application compatibility issues.

The thing I'd like to talk about in this post is STATUS_PENDING and how it impacts minifilter development. As a quick refresher, if any component in the IO path decides it might want to process an IRP at a later time (for reasons that can vary from resource usage to synchronization) it has the options of either blocking the calling thread until it can process the IRP or to return STATUS_PENDING which doesn't block the calling thread and lets the caller do something useful in the mean time. STATUS_PENDING is the preferred mechanism for doing this, but some components look at the request to figure out if it is a synchronous request or not (in other words, to figure out if it is worth releasing the thread since if the request is synchronous then the IO manager would wait for the IRP to complete anyway and releasing the thread won't do any good) using APIs such as IoIsOperationSynchronous and its minifilter counterpart FltIsOperationSynchronous and only return STATUS_PENDING if the request is not synchronous. Please note that this only applies to IRPs. FastIo and fsFilter type requests are synchronous anyway and cannot be pended. The really important thing here is that the general rule is that ANY IRP might return STATUS_PENDING and the caller should be prepared to handle this status, regardless of whether the FILE_OBJECT is opened for synchronous IO or the IRP is marked as synchronous and so on. Basically, any call to IoCallDriver can return STATUS_PENDING. In fact, driver verifier has a mode where it forces returning STATUS_PENDING for pretty much all IO just to detect drivers that are not prepared to handle it properly.

Minifilters however don't deal with IRPs directly and IO processing is a bit different. While processing a preOp a minifilter might return FLT_PREOP_PENDING to achieve a similar effect to a legacy filter returning STATUS_PENDING. Then, when the minifilter has processed the IO and is ready to return it to FltMgr it must call FltCompletePendedPreOperation (please note that this doesn't mean that it may only return FLT_PREOP_COMPLETE; the minifilter can return any of the usual FLT_PREOP_CALLBACK_STATUSes except FLT_PREOP_PENDING, which would be meaningless in this context anyway since if the minifilter is not done processing the request, why call FltCompletePendedPreOperation at all ?). During the postOp a minifilter can also do a similar thing by returning FLT_POSTOP_MORE_PROCESSING_REQUIRED to FltMgr and then by calling FltCompletePendedPostOperation when it is done with the request.

Someone that has written some legacy filters might notice that there is one significant difference between the IRP model and the minifilter model. In the IRP case, a filter cannot return STATUS_MORE_PROCESSING_REQUIRED from a completion routine if it STATUS_PENDING wasn't returned for IRP. This is usually pretty tricky in a legacy filter because it may not be possible to know for sure when the filter might want to return STATUS_MORE_PROCESSING_REQUIRED in a completion routine and so the legacy filter has to return STATUS_PENDING while processing the preOp in all possible cases where it might want to return STATUS_MORE_PROCESSING_REQUIRED and then in the completion routine decide if it wants to actually return it or not. Minifilters have it real easy here, they can just have a preOp that returns FLT_PREOP_SUCCESS_WITH_CALLBACK (to indicate they want a postOp callback) and the in the postOp callback decide whether they need to return FLT_POSTOP_MORE_PROCESSING_REQUIRED.

So how does this work? Well, FltMgr is a legacy filter and as I said before, legacy filters that couldn't know beforehand if they will want to pend the IRP during postOp processing had to return STATUS_PENDING anyway. So FltMgr calls the PreOp callbacks for all filters in the frame and if any of them registered for a postOp callback but didn't require the operation to be synchronized (since FLT_PREOP_SYNCHRONIZE is implemented by holding the thread until the request is completed by the layers below so that the postOp routine is called in the context of this same thread) then FltMgr will return STATUS_PENDING anyway. This is a very important point for this topic so I'm going to restate it: if a minifilter registers a postOp routine and if it returns FLT_PREOP_SUCCESS_WITH_CALLBACK from the preOp callback then FltMgr will return STATUS_PENDING for that operation (provided that another minifilter in the frame didn't return FLT_PREOP_SYNCHRONIZE).

So we finally get to the juicy part. Some applications have a type of bug where they open a handle for asynchronous IO and then use it in a synchronous fashion. For example, they might call CreateFile() for asynchronous IO (with the FILE_FLAG_OVERLAPPED set) and then call ReadFile() without specifying an OVERLAPPED structure. This might even work in most cases (there is KB article that describes some cases where asynchronous IO might be converted to synchronous IO: Asynchronous Disk I/O Appears as Synchronous on Windows NT, Windows 2000, and Windows XP.) and the issue might not be detected for a long time. However, as we've explained above, FltMgr might return STATUS_PENDING regardless of what a file system would return. One might expect that ReadFile would return some failure code when it detects this case, but unfortunately that's not what happens. ReadFile will check if the OVERLAPPED structure is present and if it is not then it expects that the file has been opened the synchronous IO, so if it gets STATUS_PENDING it will simply wait on the file handle. The problem in this case is the file handle will be signaled every time IO completes, so for a handle opened for asynchronous IO that is actually used asynchronously, the file handle will be signaled when ANY operation completes, so just waiting for the handle doesn't really achieve the intended effect and instead the function might return before the actual IO is completed. This is hinted at in the documentation, which states "If hFile is opened with FILE_FLAG_OVERLAPPED, the lpOverlapped parameter must point to a valid and unique OVERLAPPED structure, otherwise the function can incorrectly report that the read operation is complete."(in the page for the ReadFile() function).

So the net effect of all this corner cases is that an application that is broken in this way but has always worked because of some NTFS implementation quirks might stop working. What's even worse is the fact that it might stop working in pretty interesting and hard to detect ways. For example, consider the case when the application is writing to a file using WriteFile and it mistakenly believes the operation has completed and it frees or reuses the buffer. The actual data written in the file will be whatever happened to be in the buffer when the write is actually processed, which is a clear data corruption scenario. The same goes for ReadFile(), the application might think it has a buffer with data from a file and might get in a broken state trying to process that buffer.

Another issue is that even if a user is diligent enough to associate the presence of a filter on their system with some random file corruptions, the bug report might be something like "file gets corrupted when installing filter X". In this case the developer might have a really hard reproducing the issue because they might not have exactly the same application version as the user or they might have a different filter that masks the issue (remember that if another filter in the frame or in a frame above that frame happens to return FLT_PREOP_SYNCHRONIZE then STATUS_PENDING will not be returned so things might still work) or they don't even process IRP_MJ_READ and IRP_MJ_WRITE so they don't expect any data contents to change (but a file might become corrupt in other ways, for example if the EOF is moved improperly, which is an IRP_MJ_SET_INFORMATION call).

In terms of identifying the issue, one might use driver verifier to force the STATUS_PENDING behavior or just install the PassThrough sample in the WDK which it will also cause pretty much all operations to be pended because it always returns FLT_PREOP_SUCCESS_WITH_CALLBACK. Another way to try to identify the issue is to return FLT_PREOP_SYNCHRONIZE for the operations that a developer suspects might be causing this and see if the broken behavior still reproduces. Of course, returning FLT_PREOP_SYNCHRONIZE instead of FLT_PREOP_SUCCESS_WITH_CALLBACK when it's not necessary might have a huge performance impact on the system so it can't be used in a production environment, but it's a good way to test things nevertheless.