Hi everyone. I've just spent a couple of days debugging an issue that I think is pretty interesting and I hope publishing this post might save someone a lot of pain.
Before I go into the specifics, let me explain a bit about my setup. My filter opens files for its own use but on behalf of the user (in other words it opens files in response to certain user actions, but the user must have access to the files as well (which is different from certain kinds of filters where the filter uses files but there is no connection between the user and the files; in those cases the files are similar to file system metadata)). It does this by calling FltCreateFile and it uses OBJ_FORCE_ACCESS_CHECK to make sure that all the access checks are performed. The files that the filter opens depend on the file that the user opens and so it's possible that the filter ends up opening symlinks or volume mount points and such. In these cases the file system will return a STATUS_REPARSE and the create will be retried on a different path; the same can happen if a file system filter returns STATUS_REPARSE. If the new path happens to be located on a different volume (which is usually the case with volume mount points, though not always since one can in fact create a mountpoint for C:\ at C:\mnt for example…) then the FltCreateFile call will generally fail with STATUS_MOUNT_POINT_NOT_RESOLVED. I've discussed this at great length in my post on
Reparsing to a Different Volume in Win7 and Win8.
The problem I was running into was that when my filter tried to open a path containing a volume mount point it would fail with STATUS_ACCESS_DENIED. This behavior was different from symlinks where the call to FltCreateFile still failed with STATUS_MOUNT_POINT_NOT_RESOLVED. This was something I wanted to investigate since the STATUS_ACCESS_VIOLATION status is one I've learned to pay attention to because it generaly indicates a bug in my code. I thought that maybe I was calling FltCreateFile with some parameter that wasn't set up correctly which led to NTFS or the IO manager or the Object manager to try an invalid access, which was probably trapped in some exception handler and returned to the caller. This was definitely worth fixing since such problems can be exploited.
So I started the looong process of debugging this issue. Being an access violation I figured it comes from some improper memory access, which is different from calling some function that returns an error, because most instructions perform memory accesses so my usual approach of stepping to the next call and walking over it to see the return status would not work. Moreover, I had no guarantee that if I set a breakpoint later on I will actually hit it since any of the memory accesses up to that breakpoint could in fact be the culprit. And, to make things worse, this was in the create path for the file system, which is heavily exercised on a running system, which means that any breakpoint that I set in the debugger that's not threaded will often trigger when another thread happens to try to do a create. Anyway, to spare you the gory details, I eventually found the problem and this is what the stack looked like when the instruction I was about to execute was the one that triggered the access violation:
0: kd> kbn L0xa
# ChildEBP RetAddr Args to Child
00 b0583190 82a8f20f 92637001 92637001 b0583734 nt!ObpCaptureObjectCreateInformation+0x61
01 b05831dc 82ae2587 b0583734 924d6f78 92637001 nt!ObOpenObjectByName+0x9b
02 b0583338 82ae17f6 b0583734 00120089 92637001 nt!IopFastQueryNetworkAttributes+0x127
03 b05833a4 82a88936 92637001 dc6a18a4 b0583550 nt!IopQueryNetworkAttributes+0x40
04 b0583480 82a6926b 938e34d8 974d6f78 92637008 nt!IopParseDevice+0x115e
05 b05834fc 82a8f2d9 00000000 b0583550 00000640 nt!ObpLookupObjectName+0x4fa
06 b0583558 82a8762b b0583734 924d6f78 00010000 nt!ObOpenObjectByName+0x165
07 b05835d4 82abee29 b058371c 00120089 b0583734 nt!IopCreateFile+0x673
08 b0583630 96297b62 b058371c 00120089 b0583734 nt!IoCreateFileEx+0x9e
09 b05836bc 9631f8f1 92ed7008 938fd008 b058371c fltmgr!FltCreateFileEx2+0xba
0: kd> u
nt!ObpCaptureObjectCreateInformation+0x61:
82a78339 8a01 mov al,byte ptr [ecx]
82a7833b 833b18 cmp dword ptr [ebx],18h
0: kd> r
eax=7fff0000 ebx=b0583734 ecx=7fff0000 edx=00000000 esi=00000000 edi=9260dd94
eip=82a78339 esp=b0583154 ebp=b0583190 iopl=0 ov up ei pl nz na po nc
cs=0008 ss=0010 ds=0023 es=0023 fs=0030 gs=0000 efl=00000a02
nt!ObpCaptureObjectCreateInformation+0x61:
82a78339 8a01 mov al,byte ptr [ecx] ds:0023:7fff0000=??
So as you can see we 're trying to read one byte from ECX, which is set to
0x7fff0000. Since we're in kernel mode this will fail with access violation. Let's look a bit up the stack to see where that weird ECX value came from (and I added a couple instructions on the bottom just to get the full picture):
0: kd> ub . L0xE
nt!ObpCaptureObjectCreateInformation+0x35:
82a7830c 807d0800 cmp byte ptr [ebp+8],0 <- compare a parameter to the function with 0.
82a78310 7429 je nt!ObpCaptureObjectCreateInformation+0x63 (82a7833b) <- and jump if it's 0
82a78312 64a124010000 mov eax,dword ptr fs:[00000124h] <- look at some address
82a78318 80b83a01000000 cmp byte ptr [eax+13Ah],0 <- and see if another byte is 0
82a7831f 741a je nt!ObpCaptureObjectCreateInformation+0x63 (82a7833b) <- and if that is 0 jump to the same place as above (so we're probably exiting an IF statement)
82a78321 8bcb mov ecx,ebx
82a78323 f6c303 test bl,3 <- check if any of the least significant two bits in ebx are set
82a78326 7406 je nt!ObpCaptureObjectCreateInformation+0x56 (82a7832e)
82a78328 e8281f0d00 call nt!ExRaiseDatatypeMisalignment (82b4a255) <- and if they're set then raise an exception for data misalignment
82a7832d cc int 3 <- and look, there's a breakpoint
82a7832e a11c079b82 mov eax,dword ptr [nt!MmUserProbeAddress (829b071c)] <- load MmUserProbeAddress into EAX
82a78333 3bd8 cmp ebx,eax <- and compare it with EBX
82a78335 7202 jb nt!ObpCaptureObjectCreateInformation+0x61 (82a78339) <- and exit the IF statement if EBX is smaller than MmUserProbeAddress
82a78337 8bc8 mov ecx,eax <-move MmUserProbeAddress into ECX
82a78339 8a01 mov al,byte ptr [ecx] <- and try to read a byte from it.
82a7833b 833b18 cmp dword ptr [ebx],18h
0: kd> dp nt!MmUserProbeAddress L1
829b071c 7fff0000
Before I explain what's going on let's see what the IF is actually checking:
0: kd> dp fs:[00000124h] L1
0030:00000124 926bfd48
0: kd> !pool 926bfd48 2
Pool page 926bfd48 region is Nonpaged pool
*926bfd18 size: 2e8 previous size: 10 (Allocated) *Thre (Protected) <- this is a thread!
Pooltag Thre : Thread objects, Binary : nt!ps
0: kd> dt nt!_KTHREAD
+0x000 Header : _DISPATCHER_HEADER
+0x010 CycleTime : Uint8B
...
+0x13a PreviousMode : Char <-and at offset 0x13A we have the PreviousMode !
...
Let's piece it all together. This piece of code is trying to figure out if a buffer that it receives as a parameter (I didn't show that code but that's what's in
EBX) is accessible. It first looks at some boolean parameter and if that's TRUE then it looks at the PreviousMode and if that's UserMode then it checks the buffer for alignment (and it raises an exception if it's not aligned) and then it checks it's in user mode memory range ( smaller than MmUserProbeAddress ). If it's not smaller than MmUserProbeAddress then it just does a probe at MmUserProbeAddress. The buffer in question is the
_OBJECT_ATTRIBUTES structure, which is actually the same one as the one I pass in for the original create. This is interesting because ObOpenObjectByName can be seen twice on the stack, with the same _OBJECT_ATTRIBUTES parameter, but the first time the call to nt!ObpCaptureObjectCreateInformation works when checking the buffer and the second time it doesn't. Since the PreviousMode is UserMode in both cases, the only difference is that the boolean parameter that ObpCaptureObjectCreateInformation is called with is 1 in the first case and 0 in the second case. This parameter seems to be related to OBJ_FORCE_ACCESS_CHECK since if I don't use OBJ_FORCE_ACCESS_CHECK then the parameter is 1 in both cases and no access violation happens.
One additional thing I'd like to explain is why this check happens only for volume mount points and not symlinks. Volume mount points are different from symlinks in that there before a new IRP_MJ_CREATE is sent to the new path, for volume mount points the IO manager checks whether the user has access to the target volume. The same thing happens for directory junctions, which are similar in implementation to volume mount points. Symlinks don't perform this check and instead the new IRP_MJ_CREATE is simply issued to the new path and access checks will be performed there. Now, for volume mount points this access check takes the form of a IopQueryNetworkAttributes, which in turn is implemented as a special kind of OPEN operation (hence the call to ObOpenObjectByName). It's this second open where ObpCaptureObjectCreateInformation fails and the access violation happens.
This problem was fixed in Win8 and it was very likely a windows bug.
Before I end this post I'd like to summarize some of the things I've discussed:
- There is a windows 7 bug where if FltCreateFile tries to open a path that traverses a volume mount point or directory junction the call will fail with STATUS_ACCESS_VIOLATION. Fixed in Win8.
- This happens because of the extra access checking for volume mount points and directory junctions.
- This doesn't happen for symlinks.
- Workaround: I can't think of anything a filter could do but one posibility to address this problem is to ask customers to not use volume mount points and instead use directory symlinks.
Finally, in closing I'd like to show some code (a modification of the PassThrough sample) that calls FltCreateFile whenever the user tries to open anything that has the name "mnt" (file or folder). After the FltCreateFile call the minifilter breaks so that I get a chance to look at the status returned by the FltCreateFile. Here is the code:
NTSTATUS status;
UNREFERENCED_PARAMETER( FltObjects );
UNREFERENCED_PARAMETER( CompletionContext );
PT_DBG_PRINT( PTDBG_TRACE_ROUTINES,
("PassThrough!PtPreOperationPassThrough: Entered\n") );
if (Data->Iopb->MajorFunction == IRP_MJ_CREATE) {
UNICODE_STRING myFile = RTL_CONSTANT_STRING( L"mnt" );
OBJECT_ATTRIBUTES fileAttributes;
HANDLE fileHandle = NULL;
IO_STATUS_BLOCK ioStatus;
PFLT_FILE_NAME_INFORMATION fileName = NULL;
NTSTATUS ecpFindStatus;
ULONG targetEcpSize = 0;
__try {
//
// this is a preCreate, get the name.
//
status = FltGetFileNameInformation( Data,
FLT_FILE_NAME_OPENED | FLT_FILE_NAME_QUERY_DEFAULT,
&fileName );
if (!NT_SUCCESS(status)) {
DbgBreakPoint();
__leave;
}
//
// we need to parse the name to get the final component
//
status = FltParseFileNameInformation( fileName );
if (!NT_SUCCESS(status)) {
DbgBreakPoint();
__leave;
}
//
// Compare to see if this is a file we care about.
//
if (!RtlEqualUnicodeString( &fileName->FinalComponent,
&myFile,
TRUE )) {
__leave;
}
//
// intialize the attributes and issue the create.
//
InitializeObjectAttributes( &fileAttributes,
&fileName->Name,
OBJ_KERNEL_HANDLE | OBJ_FORCE_ACCESS_CHECK,
NULL,
NULL );
status = FltCreateFileEx2( FltObjects->Filter,
FltObjects->Instance,
&fileHandle,
NULL,
FILE_READ_DATA | SYNCHRONIZE | FILE_READ_ATTRIBUTES,
&fileAttributes,
&ioStatus,
NULL,
FILE_ATTRIBUTE_NORMAL,
FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
FILE_OPEN,
FILE_SYNCHRONOUS_IO_ALERT,
NULL,
0,
0,
NULL );
DbgBreakPoint();
} __finally {
if (fileName != NULL) {
FltReleaseFileNameInformation( fileName );
}
if (fileHandle != NULL) {
ZwClose(fileHandle);
}
}
}
//
// See if this is an operation we would like the operation status
// for. If so request it.
//
// NOTE: most filters do NOT need to do this. You only need to make
// this call if, for example, you need to know if the oplock was
// actually granted.
//
if (PtDoRequestOperationStatus( Data )) {
status = FltRequestOperationStatusCallback( Data,
PtOperationStatusCallback,
(PVOID)(++OperationStatusCtx) );
if (!NT_SUCCESS(status)) {
PT_DBG_PRINT( PTDBG_TRACE_OPERATION_STATUS,
("PassThrough!PtPreOperationPassThrough: FltRequestOperationStatusCallback Failed, status=%08x\n",
status) );
}
}