Development 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.

Any opinions expressed on this blog are Johannes' own. Refer to the respective vendor’s product documentation for authoritative information.
« Back to home