As you are speaking about a low frequency communication then something like this should do the job ( the code was not copy-pasted but was written in the quick reply form so misprints and omissions are possible )
typedef struct {
LIST_ENTRY Entry;
ULONG MessageID;
KEVENT Event;
} WaitBlock;
static LIST_ENTRY gWaitListHead; // initialize in DriverEntry
static KSPIN_LOCK gWaitListHeadLock; // initialize in DriverEntry
static ULONG gPreviousMessageID = 0;
CallUserAndWait( __in PVOID DataForApp )
{
WaitBlock* wb = ExAllocatePool( NonPagedPool, sizeof(*wb) );
RtlZeroMemory( wb, sizeof(*wb) );
KeinitializeEvent( &wb->Event, SynchronizationEvent );
//
// it is important to add in the list before calling a user mode app
// so the waiting block can be ALWAYS discovered in IOCTL handler
// and a wakeup event is not lost
//
KeAcquireSpinLock( &gWaitListHeadLock, &OldIrql );
{
wb->MessageID = ++gPreviousMessageID;
InsertTailList( &gWaitListHead, &wb->Entry );
}
KeReleaseSpinLock( &gWaitListHeadLock, OldIrql);
CallUserApplication( wb->MessageID, DataForApp );
KeWaitForSingleObject( &wb->Event, UserMode, &Timeout );
KeAcquireSpinLock( &gWaitListHeadLock, &OldIrql );
{
RemoveEntryList( &wb->Entry );
}
KeReleaseSpinLock( &gWaitListHeadLock, OldIrql);
ExFreePool( wb );
}
//
// ProcessWakeIOCTL is called from an IOCTL dispatch handler,
// MessageID is sent by an application via IOCTL or any other
// application to driver communication
//
ProcessWakeIOCTL( __in ULONG MessageID )
{
KeAcquireSpinLock( &gWaitListHeadLock, &OldIrql );
{
//
// traverse gWaitListHead find an entry by MessageID
//
for( entry = gWaitListHead->Flink; entry != &gWaitListHead; entry = entry->Flink ){
WaitBlock* wb = CONTAINING_ENTRY( entry, WaitBlock, Entry );
if( wb->MessageID != MessageID )
continue;
//
// the event must be set under the lock as KeWaitForSingleObject waits with Timeout
// and UserMode mode, the lock guaranties wb->Event validity
//
KeSetEvent( &wb->Event );
break;
}
}
KeReleaseSpinLock( &gWaitListHeadLock, OldIrql);
}