Registry Filters and Symbolic Links

I ran into an interesting case while working on a registry filter driver the other day. Boiled down to its most basic form, my driver is blocking access to registry keys by name. I watch for a name that matches one in my list then fail the open/create. The interesting case is when a registry key is a symbolic link (I.e., was created with REG_CREATE_LINK and has a REG_LINK value named SymbolicLinkValue that points to a second key).

All my filtering work takes place in the Pre-Open/Create path, and because the name coming in doesn’t actually represent what really will be opened, I can’t exactly rely on the name. So after digging into how these operations get handled, I have found some very odd behavior. I see two different behaviors for an open/create under these conditions. I created a sample filter driver to test this and tested on Windows 8 x64 and Windows 7 x64.

  1. When I try to open the link source (the key that links to a second key), I see the following:
    1. RegNtPreOpenKeyEx with CompleteName containing the name of the key – I return STATUS_SUCCESS to allow processing to continue.
    2. RegNtPostOpenKeyEx with PreInformation->CompleteName containing the key name from the SymbolicLinkValue. Status contains STATUS_REPARSE.
    3. RegNtPreOpenKeyEx with Complete Name containing the key name of the link target (from step b).
    4. RegNtPostOpenKeyEx with Status equal to STATUS_SUCCESS.
  2. After a small number of repetitions (usually 2) the above behavior stops, and is replaced with the following:
    1. RegNtPreOpenKeyEx with CompleteName containing the name of the key – I return STATUS_SUCCESS to allow processing to continue.
    2. RegNtPostOpenKeyEx with Status equal to STATUS_SUCCESS. PreInformation->CompleteName contains the original name from step a, but the Object contains a pointer to a registry object for the link target key.

Additionally, the behaviors above seem to revert after some period of time. If I run a simple registry open command for the link source a hundred times, I see behavior #1 twice and then #2 the remainder of the time. But after doing that if I let the machine sit idle for 4-5 minutes and run it again, I will again see behavior #1 twice, followed by #2 for as many times as I care to repeat it.

I e-mailed some contacts at Microsoft and they confirmed what I have been seeing. The registry maintains a key lookup cache that is intended to speed up open/create operations. One thing that the cache can do is remember symbolic links and avoid the need to reparse them every time they are opened. This is a bit frustrating for me, but it probably makes a significant difference in registry performance (which, incidentally is a heck of a lot faster than file system performance).

Unfortunately it would seem that the way to solve this problem is to store some of the data I need internally to my driver based on watching the systems behavior. So, for example, I can watch for the STATUS_REPARSE result to come back, and do something with the CompleteName value. So I could add a context to the call in the pre-open, put the original complete name in the context, then in the post-open when I see STATUS_REPARSE, I can add an entry to a list that says “Name A links to Name B”, which I can thereafter use in my pre-open. I then also have to deal with cases where the link key gets removed (I can watch for a pre-open with OBJ_OPENLINK, place a context on the object, then watch for the delete, and clean up my internal cache). I also have to deal with the case where the link gets retargeted to something else (I can watch for the pre-open with OBJ_OPENLINK, context the object, watch for the pre-set-value for REG_LINK named SymbolicLinkValue and update my cache).

I won’t show the solution code here, since its fairly complicated, and it is the intellectual property of my company, but the solution is fairly straight-forward once you understand what needs to be done. I will, however, show some of the code that I actually used to test this and grok the behavior.


First, I used my NativeTest project to create a key \Registry\Machine\Software\link_target, and then to create the key \Registry\Machine\Software\link_source using REG_OPTION_CREATE_LINK, and set the symbolic link value from link_source pointing at link_target. See my previous blog posting for more detail about how to do this using the NativeTest tool.

Here is the sample driver that actually does the registry filtering work. It doesn’t do anything beyond watch for open/create to specifically named keys, and print out what key name they are for, and then catches the post call and shows the name and status there. It’s not useful, but it helps to show the problem.

#include <wdm.h>
#include <ntstatus.h>
#include <dontuse.h>
#include <suppress.h>

#pragma prefast(disable:__WARNING_ENCODE_MEMBER_FUNCTION_POINTER, "Not valid for kernel mode drivers")


//
// Globals
//

LARGE_INTEGER MyCmCookie;

//
// Prototypes
//

extern "C" DRIVER_INITIALIZE DriverEntry;
extern "C" NTSTATUS DriverEntry( _In_ PDRIVER_OBJECT DriverObject, _In_ PUNICODE_STRING RegistryPath );

extern "C" DRIVER_UNLOAD MyDriverUnload;
extern "C" VOID MyDriverUnload( _In_ PDRIVER_OBJECT DriverObject );

extern "C" EX_CALLBACK_FUNCTION MyRegistryCallback;
extern "C" NTSTATUS MyRegistryCallback( _In_ PVOID CallbackContext, _In_opt_ PVOID Argument1,
                                        _In_opt_ PVOID Argument2 );

//
// Assign text sections for each routine.
//

#ifdef ALLOC_PRAGMA
#pragma alloc_text(INIT, DriverEntry)
#pragma alloc_text(PAGE, MyDriverUnload)
#pragma alloc_text(PAGE, MyRegistryCallback)
#endif


_Use_decl_annotations_
extern "C" NTSTATUS DriverEntry( _In_ PDRIVER_OBJECT DriverObject, _In_ PUNICODE_STRING RegistryPath )
{
  UNREFERENCED_PARAMETER( RegistryPath );

  DriverObject->DriverUnload = MyDriverUnload;

  UNICODE_STRING Altitude = RTL_CONSTANT_STRING( L"123456" );
  NTSTATUS status = CmRegisterCallbackEx( &MyRegistryCallback, &Altitude, DriverObject, NULL,
                                          &MyCmCookie, NULL );
  if ( NT_SUCCESS( status ) )
  {
  }

  return status;
}

_Use_decl_annotations_
extern "C" VOID MyDriverUnload( _In_ PDRIVER_OBJECT DriverObject )
{
  UNREFERENCED_PARAMETER( DriverObject );
  PAGED_CODE();

  CmUnRegisterCallback( MyCmCookie );
}

UNICODE_STRING SourceNameFull = RTL_CONSTANT_STRING( L"\\REGISTRY\\MACHINE\\SOFTWARE\\link_source" );
UNICODE_STRING TargetNameFull = RTL_CONSTANT_STRING( L"\\REGISTRY\\MACHINE\\SOFTWARE\\link_target" );
UNICODE_STRING SourceName = RTL_CONSTANT_STRING( L"SOFTWARE\\link_source" );
UNICODE_STRING TargetName = RTL_CONSTANT_STRING( L"SOFTWARE\\link_target" );

_Use_decl_annotations_
extern "C" NTSTATUS MyRegistryCallback( _In_ PVOID CallbackContext, _In_opt_ PVOID Argument1,
                                        _In_opt_ PVOID Argument2 )
{
  UNREFERENCED_PARAMETER( CallbackContext );
  PAGED_CODE();

  REG_NOTIFY_CLASS RegNotifyClass = (REG_NOTIFY_CLASS)(ULONG)Argument1;
  switch ( RegNotifyClass )
  {
  case RegNtPreCreateKeyEx:
  case RegNtPreOpenKeyEx:
    {
      PREG_CREATE_KEY_INFORMATION pPreOpInfo = (PREG_CREATE_KEY_INFORMATION) Argument2;
      pPreOpInfo->CallContext = (PVOID)0;
      if (
        !RtlCompareUnicodeString( pPreOpInfo->CompleteName, &SourceNameFull, TRUE ) ||
        !RtlCompareUnicodeString( pPreOpInfo->CompleteName, &TargetNameFull, TRUE ) ||
        !RtlCompareUnicodeString( pPreOpInfo->CompleteName, &SourceName, TRUE ) ||
        !RtlCompareUnicodeString( pPreOpInfo->CompleteName, &TargetName, TRUE )
        )
      {
        KdPrint(( "Pre-Open/Create for %wZ\n", pPreOpInfo->CompleteName ));
        pPreOpInfo->CallContext = (PVOID)1;
      }
    }
    break;

  case RegNtPostCreateKeyEx:
  case RegNtPostOpenKeyEx:
    {
      PREG_POST_OPERATION_INFORMATION pPostOpInfo = (PREG_POST_OPERATION_INFORMATION) Argument2;
      PREG_CREATE_KEY_INFORMATION pPreOpInfo =
                                  (PREG_CREATE_KEY_INFORMATION) pPostOpInfo->PreInformation;
      if ( pPostOpInfo->CallContext )
      {
        KdPrint(( "Post-Open/Create for %wZ (Status = %08x)\n",
                  pPreOpInfo->CompleteName, pPostOpInfo->Status ));
      }
    }
    break;

  default:
    break;
  }

  return STATUS_SUCCESS;
}

Leave a Reply

Your email address will not be published. Required fields are marked *

Complete the following to verify your humanity: * Time limit is exhausted. Please reload CAPTCHA.

This site uses Akismet to reduce spam. Learn how your comment data is processed.