If you test your drivers with Driver Verifier’s Low Resources Simulation option, you’ll soon find that memory allocations and other resource allocations are failing occasionally. If you are also testing with NDIS verifier with its low resources sim turned on, you’ll find that things fail quite often.
A basic principle of good driver design is grace under pressure
. No, I’m not referring to the Rush album, I’m referring to the quality that a good driver has that allows it to be maximally useful in low-resources conditions.
This is a commonly overlooked area of driver design. It doesn’t simply imply checking return values (although you should, every time). It further means making intelligent micro-design decisions and intelligent high-level design decisions.
An example of a bad micro-design decision (pesudo-code):
PUCHAR g_array;
BOOLEAN updateArray(PUCHAR newArray, SIZE_T newSize)
{
if(g_array)
ExFreePool(&g_array);
g_array = ExAllocatePoolWithTag(
PagdPool,
newSize,
'0mem');
if(!g_array)
return FALSE;
RtlCopyMemory(
g_array,
newArray,
newSize);
return TRUE;
}
It’s good that this code checks the return value of the allocate – otherwise it’d be a bugcheck on failure – but it has weird side effects. As a user of this function, I’d expect at a glance that it would either successfully update the array and return TRUE or fail to update the array and return FALSE. Instead, it updates the array to something totally odd (NULL) and returns FALSE. And, as a bonus (not!), if this array was adding value to your driver, then you’ve killed that in the process.
Obviously each failed allocation case needs to be considered independently, but there are a couple of techniques you can use to reduce failures like these. One thing that works more often than you might expect is to simply statically allocate resources once and re-use them. In the above example, I could have simply declared a global array that was big enough to handle my expected needs, and perhaps added some bounds checking to the mix. In things like device extensions or network adapter context areas, you can often reserve enough extra space the first time that you don’t have to free and re-allocate space after the initial allocation.
Another tool that you may be able to bring to bear is the lookaside list – reference ExInitializePagedLookasideList and friends. If you’re allocating and releasing fixed-size hunks of data, then using a lookaside list will help smooth out your requests to the OS for memory, and may result in fewer out-of-memory conditions being hit. Note, however, that this is totally OS-dependent, in that the OS can decide to shrink your free list whenever it wants if it is under memory pressure.
Using a lookaside list can have a couple of other benefits too: since you tend to build up your cache of blocks early in your driver’s execution, you can avoid resource constraints due to pool fragmentation, and on the flip side, using a lookaside list keeps your driver from contributing to the fragmentation. And, of course, there are performance benefits.
At the end of the day, it’s far more important to design your driver to gracefully handle low-resources conditions than it is to push the problem off onto the lookaside list code, but lookaside lists are worth thinking about next time you are evaluating a driver design.