BDE 4.14.0 Production release
|
Provide a memory allocator that guards against buffer overruns.
This component provides a concrete allocation mechanism, bdlma::GuardingAllocator
, that implements the bslma::Allocator
protocol and adjoins a read/write protected guard page to each block of memory returned by the allocate
method. Each returned block is maximally aligned for the platform. The guard page is located immediately following (subject to alignment requirements) or immediately preceding the block returned from allocate
according to an optionally-supplied constructor argument:
WARNING: Note that this allocator should not be used for production use; it is intended for debugging purposes only. In particular, clients should be aware that a multiple of the page size is allocated for each allocate
invocation (unless the size of the request is 0).
Also note that, unlike many other BDE allocators, a bslma::Allocator *
cannot be (optionally) supplied upon construction of a GuardingAllocator
; instead, a system facility is used that allocates blocks of memory in multiples of the system page size.
A GuardingAllocator
may be used to debug buffer overflow (or underflow) by protecting a memory page after (or before) each block of memory returned from allocate
. Consequently, certain memory access outside the block returned to the client will trigger a memory protection fault.
A constructor argument of type GuardingAllocator::GuardPageLocation
, an enumeration, determines whether guard pages are located following (e_AFTER_USER_BLOCK
) or preceding (e_BEFORE_USER_BLOCK
) the user block. If no value is supplied at construction, e_AFTER_USER_BLOCK
is assumed.
To illustrate, the following diagram shows the memory layout resulting from an N
-byte allocation request from a guarding allocator, where N
is assumed to be less than or equal to the size of a memory page. Note that two pages of memory are consumed for each such allocation request:
Notice that M
, the distance from the returned address to the start of the guard page, may be larger than the user requested N
bytes. See {Example 2: Allowing for Maximal Alignment}.
The bdlma::GuardingAllocator
class is fully thread-safe (see bsldoc_glossary ).
This section illustrates intended use of this component.
Use of a bdlma::GuardingAllocator
is indicated, for example, if some code under development is suspected of having a buffer overrun (or underrun) bug, and more sophisticated tools that detect such conditions are either not available, or are inconvenient to apply to the situation at hand.
This usage example illustrates a guarding allocator being brought to bear on a buffer overrun bug. The bug in question arises in the context of an artificial data handling class, my_DataHandler
. This class makes use of a (similarly artificial) data translation utility that translates chunks of data among various data styles. In our idealized example, we assume that the length of the output resulting from some data translation is precisely determinable from the length of the input data and the respective styles of the input and the (desired) output. For simplicity, we also assume that input data comes from a trusted source.
First, we define an enumeration of data styles:
Next, we define the (elided) interface of our data translation utility:
Next, we define my_DataHandler
, a simple class that makes use of my_DataTranslationUtil
:
Next, we show the definition of the my_DataHandler
constructor:
Next, we show the definition of the generateAlternate
manipulator. Note that we have deliberately introduced a bug in generateAlternate
to cause buffer overrun:
Next, we define some data (in e_STYLE_A
):
Then, we define a my_DataHandler
object, handler
, to process that data:
Note that our handler
object uses the default allocator.
Next, we request that an alternate data style, e_STYLE_AA
, be generated by handler
. Unfortunately, data in style e_STYLE_AA
is twice as large as that in style e_STYLE_A
making it a virtual certainty that the program will crash due to the insufficiently sized buffer that is allocated in the generateAlternate
method to accommodate the e_STYLE_AA
data:
Suppose that after performing a brief post-mortem on the resulting core file, we strongly suspect that a buffer overrun is the root cause, but the program crashed in a context far removed from that of the source of the problem (which is often the case with buffer overrun issues).
Consequently, we modify the code to supply a guarding allocator to the handler
object, then rebuild and rerun the program. We have configured the guarding allocator (below) to place guard pages after user blocks. Note that e_AFTER_USER_BLOCK
is the default, so it need not be specified at construction as we have (pedantically) done here:
With a guarding allocator now in place, a memory fault is triggered when a guard page is overwritten as a result of the buffer overrun bug. Hence, the program will dump core in a context that is more proximate to the buggy code, resulting in a core file that will be more amenable to revealing the issue when analyzed in a debugger.
The requirement that this allocator always return maximally aligned memory can lead to situations when using e_AFTER_USER_BLOCK
where there is unused memory between the end of allocated memory and the first address of the guard page. If so, small memory overruns (e.g., a single byte) will not land on the guard page and go undetected. Fortunately, users can often compensate for this behavior and position their data adjacent to the guard page.
Suppose one must test a function, myIntSort
, having the signature and contract:
If the myIntSort
function uses some manner of partitioning algorithm the implementation will involve considerable pointer arithmetic, recursion, etc., then a reasonable test concern would be:
Addressing that test concern is ordinarily challenging. One approach is to bracket the data for each test with data having a distinctive value (e.g., 0x0BADCAFE
) and then check that the test does not corrupt that pattern (any overwrite being very unlikely to preserve the special value). Tests of reads past the given range are harder to prove. One could argue that incorporating that data into the sort would corrupt the result but one cannot prove that it was never accessed. Alternatively, using bdlma::GuardingAllocator
provides a stronger proof from a simpler test case. Thus, our test plan would include:
First, create a set of test data for thoroughly testing all concerns of myIntSort
, and a framework for running through those tests:
Then, create a 'bdlma::GuardingAllocator to that will be used to test for under-runs of the given range and, for each data point, run the test on data that will segfault if there is any reference to an address in the page below begin
, even by a single byte:
Notice that, for expository purposes, we confirmed that the block
is page aligned.
Next, we will rerun the test using data positioned in memory to catch over-runs of the input range.
The step would be to allocate memory and initialize memory as we did before. The problem is that memory returned from the bdlma::GuardingAllocator
may not abut the following guard page.
Consider a platform where:
sizeof(int)
is 4 bytes.For the data point above consisting of 3 values, the required space is 12 bytes (3 * 4) but the maximally aligned address closest to the top of the returned page is 16 bytes (2 * 8) below the page boundary – a gap of 4 bytes.
We handle this situation by padding our allocation size to the nearest multiple of maximal alignment – 16 bytes in this case. That gives us allocated memory that abuts the page boundary. This allows us to position our test data into the allocated memory so that last element fits in the upper bytes of the returned bytes (i.e., the first 4 bytes of the returned block are not used by this test).
Now, we calculate the padded allocation size and allocate a block that abuts the page boundary:
Notice that again, for purposes of exposition, we have checked the returned addresses and confirmed:
block
, is maximally aligned.begin
is correctly aligned to hold the data value.end
, is page aligned.Finally, we load the test data into the carefully positioned memory and rerun the test: