X

Improve application security, performance, and scalability

  • October 14, 2015

Let's Get The Low Hanging Fruits - Static detection of memory access errors using Previse

Raj Prakash
Architect
There is a subset of memory access errors that can be detected at compile time. This is obviously the ideal time to warn about these issues because the developer will see the issue and have time to correct it before the application reaches the hands of the user. However, compile time analysis is limited to those errors which can be determined by a careful analysis of a region of source code. The Solaris Studio Code Analyzer User Guide has a list of the types of errors that can be identified through static analysis.



An example of the kind of error that can be identified through static analysis is the use of uninitialized data shown in the following code:

#include <stdio.h>
void main()
{
int i;
printf("i = %i\n", i);
}

The reporting of these kinds of static errors is a two step process. The first step is to compile the application with the flag -xprevise that enables static analysis. The second step is to use one of the two tools that produce a report on the detected errors. The tool codean produces a textual report, the code-analyzer tool presents the data in a GUI. The following output shows compiling the code and seeing the textual report.

$ cc -g -xprevise uni.c
$ codean a.out
STATIC report of a.out:
ERROR 1 (UMR): accessing uninitialized data: i at:
main() <uni.c : 6>
3: void main()
4: {
5: int i;
6:=> printf("i = %i\n", i);
7: }
PREVISE SUMMARY for a.out: 1 error(s), 0 warning(s), 0 leak(s) in total

The following figure shows the same report in the code-analyzer GUI:






In the GUI, the type of error is shown as the title, the line containing the error is highlighted in grey.
Although static analysis is relatively limited in what it can achieve, it will report errors on all the previously shown code snippets. Consider the earlier code containing an access to an uninitialized variable.

ERROR 1 (UMR): accessing uninitialized data: i at:
main() <uninit.c : 11>
8: void main()
9: {
10: int i;
11:=> printint(&i);
12: }
PREVISE SUMMARY for a.out: 1 error(s), 0 warning(s), 0 leak(s) in total

Note that the error is reported at the point where the variable is passed into the printint() routine. This is not a speculative error, the tool has actually looked into printint() and identified that the variable i is read there, and so passing in an uninitialized variable is an error. If the code is changed so that the variable is initialized in the routine, then the compiler no longer reports the function call as a problem. It is able to do this kind of detailed analysis because it can see the bodies of both the calling and called functions, and is therefore able to see both how the variable is initialized and how it is later used.



For the code where there is an access to previously freed memory, the tool reports both the location of the access and the location where the memory was freed. This greatly helps the developer find the program flow where there is a problem, and makes it much easy to resolve the issue.

WARNING 1 (FMR): reading from freed memory: i at:
main() <freed.c : 13>
10: {
11: int * i = malloc(sizeof(int));
12: free(i);
13:=> printint(i);
14: }
was freed at:
main() <freed.c : 12>
9: void main()
10: {
11: int * i = malloc(sizeof(int));
12:=> free(i);
13: printint(i);

However, this is not the only issue that the tool picks up in this code. It also identifies a missing check for malloc() returning a null pointer. This missing return error is quite common in code, and is relatively easy for the compiler to identify. It is worth noting that in the event of an error the program should check the errno variable to determine whether whether the error condition is fatal or indicates an action that should be retried. In the case of malloc() it is possible for the OS to report that it currently has insufficient memory available, but the program should retry the allocation operation in the expectation that more memory will have been made available.

WARNING 2 (MRC): missing null-pointer check after malloc: malloc(4) at:
main() <freed.c : 11>
7: }
9: void main()
10: {
11:=> int * i = malloc(sizeof(int));
12: free(i);
PREVISE SUMMARY for ./a.out: 0 error(s), 2 warning(s), 0 leak(s) in total

The earlier example which shows an access past array bounds is too complex for the tool to diagnose because in this example the bounds checking would require the tool to iterate through the loop, so detecting this error would require dynamic checking of the code. However, the tool does pick up two other issues with the code. Once again the code contains no check of the return from malloc(). The second issue is that the tool detects a memory leak where the allocated memory is not freed.

LEAK 1 : 1 block left allocated on heap with a total size of 4 bytes
main() <array.c : 14>
10: }
12: void main()
13: {
14:=> int * array = (int*) calloc(sizeof(int), 10);
15: printarray(array,11);

WARNING 1 (MRC): missing null-pointer check after malloc: calloc(4,10) at:
main()
10: }
12: void main()
13: {
14:=> int * array = (int*) calloc(sizeof(int), 10);
15: printarray(array,11);
PREVISE SUMMARY for ./a.out: 0 error(s), 1 warning(s), 1 leak(s) in total

It is impossible for static analysis of applications to identify all the errors that exist in an application. The risk is that more extensive static analysis of the code may end up raising many false positives. However, there are a surprising number of real errors that can be identified. The big advantage of using this kind of detection is that it all happens at compile time long before any users have had the chance to try the code.

Be the first to comment

Comments ( 0 )
Please enter your name.Please provide a valid email address.Please enter a comment.CAPTCHA challenge response provided was incorrect. Please try again.