Detecting accesses outside the bounds of arrays and other objects
Description
Checks that accesses through pointer expressions are within the bounds of the expected object. The object can be of any type and can reside anywhere—globally, on the stack, or on the heap.
Why perform the check
The check is useful whenever your application reads or writes to locations it should not. For example:
int arr[10] = {0};
int f(int i)
{
return arr[i];
}
int g(void)
{
return f(20); /* arr[20 is out of bounds] */
}How to use it
Compiler option: ‑‑runtime_checking bounds
In the IDE: Project>Options>Runtime Checking>Enable bounds checking
This will enable out-of-bounds checking globally. Note that there are suboptions that you can use to fine-tune the out-of-bounds checking globally and for each source file.
How it works
In code where pointer bounds are tracked:
Each transfer of a pointer value also transfers the bounds for that pointer value.
When a pointer is initialized to point to an object of some sort, the bounds of the pointer are set to the bounds of the object. If the object is an array, the bounds cover the entire array. If it is a single instance, the bounds cover the single instance.
When a pointer is initialized to an absolute address, the pointer is assumed to point to a single object of the specified type. For example:
uint32_t * p = (uint_32_t *)0x100;
In this case,
pwill point to a 32-bit unsigned integer at address0x100, with the bounds0x100and0x104.A null pointer is given bounds that do not cover any access, in other words, an access through it is erroneous.
When a pointer value is passed to a function as a parameter, the bounds are passed as extra, hidden, parameters.
When a pointer value is returned from a function, the returned value and the bounds are passed in a
structas the actual return value.When a pointer value is stored in memory in such a way that it can be accessed via pointers, its bounds are stored in a global bounds table. Whenever the pointer value is accessed, the associated bounds in the global bounds table are retrieved as well. The size of the global bounds table can be changed using Number of entries (the linker option
‑‑bounds_table_size number_of_records[:number_of_buckets]|(number_of_bytes)).In other cases, the bounds are kept track of in extra local variables.
For each access through a pointer expression, the calculated address and the calculated address plus the access size is checked against the bounds. If any of the two addresses are outside of the bounds, a C-RUN message is generated.
Functions that receive pointers in any parameters, or that return a pointer value, can exist in two variants, one with the bounds, and one without the bounds.
Resource usage
The bounds checking overhead can cause the application to no longer fit in the available ROM or RAM. There are some ways you can try to deal with this:
Provided that your application does not use too many indirectly accessed pointers, you can shrink the global bounds table to reduce the amount of RAM used for it. See ‑‑bounds_table_size (in the IDE, Number of entries).
By default, 4-Kbyte entries that need about 190 Kbytes are used.
You can turn off the actual bounds checks in some modules. This will reduce the amount of code added by instrumentation to some extent.
You can turn off pointer bounds tracking in some modules. This will eliminate the increase in code size entirely in these modules, but will cause problems in the interface between the code that does track pointer bounds and the code that does not. See the next section for more information.
Non-checked code
Sometimes you cannot enable bounds checking in the entire application, for example if some part of the application is an externally built library, or is written in assembler. If you add any extra source code lines to make your code work for bounds checking, use the preprocessor symbol __AS_BOUNDS__ to make the extra source code conditional. These are some cases you should consider:
Calling code that does not track bounds from code that does
This only affects functions with pointers as parameters or as return types.
By using
#pragma no_boundsor#pragma default_no_boundson your declarations. you can specify that certain functions do not track pointer bounds. If you call such a function from code that does not track pointer bounds, no extra hidden parameters are passed, and any returned pointers are either considered “unsafe” (all checked accesses via such pointers generate errors) or “safe” (accesses via such pointers cannot fail), depending on whether the option Check pointers from non-instrumented functions has been used or not (compiler option‑‑ignore_uninstrumented_pointers). If you wish to explicitly specify the bounds on such values, use the built in operator__as_make_bounds.For example:
#pragma no_bounds struct X * f1(void); ... { struct X *px = f1(); /* Set bounds to allow acesses to a single X struct. (If the pointer can be NULL, you must check for that.) */ if (px) px = __as_make_bounds(px, 1); /* From here, any accesses via the pointer will be checked to ensure taht they are within the struct. */Calling code that tracks bounds from code that does not
If you call a function that tracks bounds, and which has pointers as parameters, or which returns a pointer, from code that does not track bounds, you will generally get an
undefined externalerror when linking. To enable such calls, you can use#pragma generate_entry_without_boundsor the option Generate functions callable from non-instrumented code (compiler option‑‑generate_entries_without_bounds) to direct the compiler to emit one or more extra functions that can be called from code that does not track bounds. Each such function will simply call the function with default bounds, which will be either "safe" (accesses via such pointers never generate errors) or "unsafe" (accesses via such pointers always generate errors) depending on whether the option Check pointers from uninstrumented functions (compiler option‑‑ignore_uninstrumented_pointers) has been used or not.If you want to specify more precise bounds in this case, use
#pragma define_without_bounds.You can use this pragma directive in two ways. If the function in question is only called from code that does not track pointer bounds, and the bounds are known or can be inferred from other parameters, there is no need for two functions, and you can simply modify the definition using
#pragma define_without_bounds.For example:
#pragma define_without_bounds int f2(int * p, int n) { p = __as_make_bounds(p, n); /* Give p bounds */ ... }In the example,
pis assumed to point to an array ofnintegers. After the assignment, the bounds forpwill bepandp + n.If the function can be called from both code that does track pointer bounds and from code that does not, you can instead use
#pragma define_without_boundsto define an extra variant of the function without bounds information that calls the variant with bounds information.You cannot define both the variant without bounds and the variant with bounds in the same translation unit.
For example:
#pragma define_without_bounds int f3(int * p, int n) { return f3(__as_make_bounds(p, n), n); }In the example,
pis assumed to point to an array ofnintegers. The variant off3without extra bounds information defined here calls the variant off3with extra bounds information ("f3[with bounds]"), giving the pointer parameter bounds ofpandp + n.Global variables with pointers defined in code that does not track bounds
These pointers will get either bounds that signal an error on any access, or, if the option Check pointers from non-instrumented memory (linker option
‑‑ignore_unistrumented_pointers) is used when linking, bounds that never cause an error to be signaled. If you need more specific bounds, use__as_make_bounds.For example:
extern struct x * gptr; int main(void) { /* Give gptr bounds with size N. */ gptr = __as_make_bounds(gptr, N); ... }RTOS tasks
The function that implements a task might get called with a parameter that is a pointer. If the RTOS itself is not tracking pointer bounds, you must use
#pragma define_without_boundsand__as_make_boundsto get the correct bounds information.For example:
#pragma define_without_bounds void task1(struct Arg * p) { /* p points to a single Arg struct */ p = __as_make_bounds(p, 1); ... }
Some limitations:
Function pointers
Sharing a function pointer between code that tracks bounds and code that does not can be problematic.
There is no difference in type between functions that track bounds, and functions that do not. Functions of both kinds can be assigned to function pointers, or passed to functions that take function pointer parameters. However, if a function whose signature includes pointers is called in a non-matching context (a function that tracks bounds from code that does not, or vice versa), things will not work reliably. In the most favorable cases, this will mean confusing bounds violations, but it can cause practically any behavior because these functions are being called with an incorrect number of arguments.
For things to work, you must ensure that all functions whose signature includes pointers, and which are called via function pointers, are of the right kind. For the simple case of call-backs from a library that does not track bounds, it will usually suffice to use
#pragma no_boundson the relevant functions.K&R functions
Do not use K&R functions. Use
‑‑require_prototypesand shared header files to make sure that all functions have proper prototypes. Note that in Cvoid f()is a K&R function, whilef(void)is not.Pointers updated by code that does not track bounds
Whenever a pointer is updated by code that does not set up new bounds for the pointer, there is a potential problem. If the new pointer value does not point into the same object as the old pointer value, the bounds will be incorrect and an access via this pointer in checked code will signal an error.
Absolute addresses
If you use #pragma location or the @ operator to place variables at absolute addresses, pointers to these variables will get correct bounds, just like pointers to any other variables.
If you use an explicit cast from an integer to a pointer, the pointer will get bounds assuming that it points to a single object of the specified type. If you need other bounds, use __as_make_bounds.
For example:
/* p will get bounds that assume it points to a single struct Port at address 0x1000. */ p = (struct Port *)0x1000; /* If it points to an array of 3 struct you can add */ p = __as_make_bounds(p, 3);
Example
Follow the procedure described in Getting started using C-RUN runtime error checking, but use the Bounds checking option.
This is an example of source code that will be identified during runtime:
C-RUN will report either Access out of bounds or Invalid function pointer. This is an example of the message information that will be listed: