Checking for buffer overflows in unit tests

Posted on

Functions that take a buffer and a corresponding buffer size as a parameter are ubiquitous. As an example, consider this function declaration:

    /**
      Routine Description:
        ...queries some value...
    
      Parameters:
        BufferSize - size of buffer in WCHARs.
        Buffer     - buffer to store information in.
    
      Return Value:
        ERROR_SUCCESS on success.
        ERROR_INSUFFICIENT_BUFFER if the buffer is to small.
    --*/
    DWORD QueryInformation(
      __in UINT BufferSize,
      __out_ecount(BufferSize) PWSTR Buffer
      );
    ```



If we are to write a testcase for this function, it is important to check that the function properly handles buffers of insufficient size. Let us say that we expect this function to return a file path, so its is safe to assume that a buffer size of 2 is guaranteed to be too small and a buffer size of MAX_PATH should be large enough. A straightforward testcase could thus look like this:



 WCHAR SmallBuf[ 2 ];
 WCHAR ReasonableBuf[ MAX_PATH ];

 //
 // Use buffer that is guranteed to be too small.
 //
 TEST( ERROR_INSUFFICIENT_BUFFER == QueryInformation( 
   _countof( SmallBuf ), 
   SmallBuf ) );

 //
 // Use a reasonable buffer size.
 //
 TEST( ERROR_SUCCESS == QueryInformation( 
   _countof( ReasonableBuf ), 
   ReasonableBuf ) );

 ... test contents of ReasonableBuf ...

```

If the buffer size handling of QueryInformation is fundamentally flawed, this test case will catch the error. However, QueryInformation may contain a more subtle flaw such as an off-by-one error, that only manifests itself when the buffer is exactly one element too small. In this case odds are that this test case will fail to notice the mistake.

A handy idiom to overcome this is to use brute-force and just test all buffer sizes from 0 to a reasonable maximum size (MAX_PATH in this case). In order to catch buffer overflows, we use the same trick as employed by most debug heaps – we pad the end of the buffer with a guard value and make sure that the value is still intact after the function has used the buffer. An off-by-one error is now highly likely to be caught. The test case may look like this:

> WCHAR Buffer[ MAX_PATH ]; > UINT BufferSize; > > // > // Try all buffer sizes. > // > BOOL WorkedAtLeastOnce = FALSE; > for ( BufferSize = _countof( Buffer ); > BufferSize 0; BufferSize-- ) > { > DWORD Result; > > // > // Write guard value to end of buffer.. > // > Buffer[ BufferSize - 1 ] = 0xDEAD; > > // > // Pass the buffer to the function under test. > // > Result = QueryInformation( > BufferSize - 1, // account for guard value. > Buffer ); > > // > // Check that guard value is intact - regardless of whether > // function failed or not. > // > TEST( Buffer[ BufferSize - 1 ] == 0xDEAD ); > > // > // Expect either ERROR_INSUFFICIENT_BUFFER or > // ERROR_SUCCESS > // > TEST( Result == ERROR_INSUFFICIENT_BUFFER || > ( Result == ERROR_SUCCESS && > ... check contents of Buffer ... ) ); > > WorkedAtLeastOnce |= ( Result == ERROR_SUCCESS ); > } > TEST( WorkedAtLeastOnce ); > > > Of course, it is now important to make sure that the function under test succeeded at least once, hence the additional check.

« Back to home