14. Appendix

14.1. Inspector Reference

14.1.1. How Does Inspector Work?

At its heart, Inspector's technology is most like a whole-program "optimizer": it uses extended versions of algorithms familiar to those writing compiler optimizers. First, a conventional compiler front end is used to convert the program into a treelike intermediate language, called "SCIL" (Statically Checkable Intermediate Language). Then, the Inspector "engine" takes over and begins with a global aliasing analysis followed by translation of each subprogram into a static single assignment (SSA) representation and then a global value numbering over the subprogram. At this point, most compiler optimizers would begin the process of generating code. Inspector instead begins the process of determining whether the program might violate any run-time checks or user assertions and whether the algorithms have any "race conditions" or security "holes."

Inspector analyzes each subprogram separately, by taking into account only the information computed for callees of the subprogram. In particular, the analysis of a subprogram does not depend on its callers. To perform this analysis, Inspector, using the global value numbering, determines the possible value set for every variable and expression appearing in the program at every point where it appears. It uses this information to determine where checks or assertions might fail and also uses it to determine the "preconditions" and "postconditions" of every subprogram. The preconditions represent what must be true about the inputs to the subprogram for the subprogram's algorithm to operate without overflowing or failing a run-time check. The postconditions represent what is true about the outputs after the subprogram completes, presuming that the inputs satisfied the preconditions.

Once the pre- and postconditions for a subprogram have been determined, they can be propagated to every point where the subprogram is called, iterating as necessary in the presence of recursion, until the whole program has been analyzed. Once this process stabilizes, a final pass is performed to report any potential run-time or assertion failures, race conditions, or security holes that exist anywhere within the program. During that pass, Inspector may generate error messages whenever a caller could violate the precondition generated for a callee to guard against an error in the callee. As a result, an error may not be reported by Inspector at the point where there is a fault in the program, but rather at the point where this fault results in an inconsistency. Reviewing the preconditions at this point allows identifying whether the caller or the callee is the culprit by matching the preconditions generated by Inspector and the expectations of the programmer. Reviewing the preconditions generated by Inspector is another way of detecting errors before they occur in the program.

The Inspector technology is fundamentally a "bottom up" approach, which provides excellent scalability. The complexity of the analysis is not dependent on the number of paths in the call graph but rather merely on the number of subprograms in the graph, with a factor based on the amount of recursion. The technology is also very precise, in that the pre- and postconditions for every subprogram include information about all inputs and outputs, including all global variables and objects reachable through chains of pointers. This precision helps to reduce the false-positive rate while at the same time avoiding false negatives.

The book Static Analysis of Software: The Abstract Interpretation, published by Wiley (2012), contains a chapter on the mathematics underlying the inner working of Inspector.

14.1.2. Partitioning of Analysis

When analysis a large set of sources and depending on how much memory is available, Inspector may decide to split the analysis into multiple partitions, consisting of a subset of the sources.

The partitioning is done based on the following criteria:

  1. When using Inspector's -global switches, the analysis is always done globally, with a single partition.

  2. Otherwise, based on the size of the sources the analysis may be split into multiple partitions. This is done using a call-graph based analysis which attempts to minimize the number of cases where a caller and a callee are assigned to different partition elements. The default size of partitions is set per level to use a certain maximum amount of memory:

    Level

    Memory limit per partition

    1

    1GB

    2

    3GB

    3

    4GB

    4

    Unlimited

    This can be further tuned via Inspector's --partition-memory-limit switch, e.g. --partition-memory-limit 8GB.

Each partition is saved in a file called part-xxx-of-project.library where xxx is the partition number (e.g. 001, 002, etc...).

For subsequent analysis, the previous partition files - if found - are reused from one run to another, except that files no longer present in a new run are removed, and new files are appended at the end of the last partition, or in a new partition if the last partition was already full.

Note that it is more likely that a global analysis might run out of memory during the processing of some source file. In that case, the source file is skipped and analysis proceeds as though that file was not present in the library, unless memory is really stressed too far, in which case the entire analysis may fail.

There is also a switch --project-partitions which is provided. With this switch, in deep analysis mode, at least one partition is created for each project file (.gpr) in your project hierarchy, assuming you are using more than one project file. In other cases (fast analysis mode or a single project file), this switch is ignored. Specification of --project-partitions is not usually recommended unless partitioning compatibility with older versions of Inspector is desired, or if you want to manually influence Inspector's partition selection algorithm. When this switch is used, the partition files are regenerated after each run (as opposed to being reused run after run).

Finally, the optional switch --simple-partitions disables partitioning algorithm based on a call-graph analysis and assigns source files to partitions based on an alphabetical order.

14.1.3. Inspector Limitations and Heuristics

The most accurate static error detection is possible when the entire program representation is available to Inspector. There are two reasons why the entire program might not be available: a portion of the program might simply not be available: perhaps it hasn't been written yet or perhaps it's from a restricted library; the other reason is that Inspector might have to partition the program representation into manageable sized partitions.

Inspector performs static error detection by holding in main memory a representation of the entire subsystem being analyzed. It is possible that the subsystem being analyzed is so large that its representation cannot be contained in the memory of the host machine on which Inspector is running. Inspector automatically divides the subsystem being analyzed into partitions that are more likely to be analyzable within the memory constraints of the host machine. See Partitioning of Analysis for more details on how Inspector decides to partition an analysis.

14.1.3.1. Evaluation Order

Inspector assumes that arguments of calls are evaluated in left-to-right order. In practice, compilers are allowed to choose any evaluation order for arguments of calls. GNAT in particular may evaluate call arguments in different orders depending on the target platform. If the order of evaluation matters for the side effects of the call, then Inspector analysis may not be sound with respect to the behavior observed when executing on the target.

14.1.3.2. Inspector Presumptions

Sometimes the entire program is not available to Inspector, either because the program source is simply not available, or because the program was so large it needed to be partitioned. In either of these cases, important information about a subprogram invocation might not be available to Inspector at the point of subprogram invocation. Inspector will not be able to calculate the preconditions of the subprogram being called and it will not be able to precisely characterize the return value nor the effects of calling that subprogram. Invocations of subprograms that haven't been processed by Inspector are referred to as unanalyzed subprogram calls.

Inspector makes presumptions about the return values and other side effects of unanalyzed subprogram calls. The presumptions of each unanalyzed subprogram call are displayed in the subprogram information block of the calling subprogram. The source line number of the unanalyzed subprogram call is shown after an @ sign, and information about its presumed return value or other side effect is displayed.

The actual unanalyzed call might be directly on the line specified in the presumption (by the @nnn) or it might be a call that occurs inside the subprogram called on the given line. You can tell if it's an indirect call or a direct call by the name given in the presumption. If it doesn't correspond to the name of the subprogram directly called on the given line, then it must be something called indirectly.

Sometimes the presumptions made by Inspector are overly "optimistic", such as a given call will never return a null, when in fact it can return a null. For example, suppose that under some circumstances, an unanalyzed subprogram call can return a null, but that the calling subprogram never checks for this possibility, and instead dereferences the return value without checking. This could cause an exception when the program is run, and implies that the program being analyzed contains a latent defect. If Inspector makes the presumption that the called subprogram does not return null, then it would not identify this latent defect. For this reason, the programmer should check the presumptions about unanalyzed subprogram calls and verify that they are appropriate.

Alternatively, the presumptions made by Inspector might be "insufficient". For example Inspector might presume that some unanalyzed subprogram call could return null, when in fact it never does. Later, in some use of the returned value, far away from the unanalyzed call, Inspector might complain that a possibly-null value is being dereferenced. The original presumption was insufficient in this case, because it didn't match the actual behavior of the unanalyzed call, and thereby led to the complaint. The programmer can eliminate such a complaint by adding code immediately after the point of the call to assert that the returned value is not null. This will cause Inspector to tighten up its presumption about the unanalyzed call to satisfy the assertion, and the complaints at other points in the code will go away. For example:

Result := Call_To_Unknown_Function (...);
pragma Assert (Result /= null);

If you do not like Inspector's default handling of presumptions, you can disable this behavior via the --no-presumptions switch.

14.1.3.3. Handling of Generic Units

Inspector does not analyze generic units per se, since it doesn't have sufficient context in order to perform a precise enough analysis of the generic code (since it is dependent on its generic parameters which can change significantly the code generated).

Instead, Inspector analyzes each instantiation of generic units. This means in particular that if you do not instantiate a generic unit, it won't be analyzed. In addition if there are multiple instantiations of a single generic unit, Inspector will analyze each instantiation separately. Note that this is supported by Inspector, but in general not by the other analysis engines. For example, this is not yet supported by Infer as described in the section Configuring Infer.

Any messages generated during the analysis of one instantiation might very likely be similar or identical to those generated for other instantiations. The net effect is that there may be multiple similar messages associated with a single line in the source listing for a generic unit.

In addition, when using e.g. the --no-subprojects switch (analyze only sources from the root project), since instantiations are analyzed in the context of the client code (where the instantiation is located), you will get an analysis and potentially messages related to source files located outside the root project.

14.1.3.4. Loop Unrolling

In some cases, Inspector may decide that it is worthwhile to unroll a loop when generating the SCIL representation of the loop. For example, given Ada source code

for I in 1 .. 3 loop
   A (I) := I * 100;
end loop;

Inspector might (or might not) choose to represent this in the generated SCIL as something more like

A (1) := 100;
A (2) := 200;
A (3) := 300;

In most cases, this sort of thing is an internal implementation decision which the user need not be aware of. However, Inspector might then go on to emit messages which pertain to one specific iteration of the loop (or, as is discussed later, to iterations other than the first iteration). In such cases, Inspector will include additional text in each message in order to unambiguously identify the subject of the message. This additional text may take either of two forms. In the case of a for loop (or of an implicitly looping construct such as a quantified expression) with static bounds for which Inspector decides full unrolling is desirable, text such as "(iteration 3 of 5)" would be included. In the case of a loop for which only the first iteration (or perhaps only some part of the first iteration, such as the initial test condition evaluation for a while loop) is unrolled, text such as "(initial iteration)" or "(subsequent iterations)" would be included. In either case, such additional text refers to the nearest enclosing loop (or implicitly looping construct).

For example, given the subprogram

function Foo return Integer is
   type Vec is array (1 .. 3) of Integer;
   Nums : Vec := (1, 2, 3);
   Dens : Vec := (-1, 0, 1);
   Result : Integer := 0;
begin
   for I in Vec'Range loop
      Result := Result + (Nums (I) / Dens (I));
   end loop;
   return Result;
end Foo;

Inspector generates the message:

foo.adb:8:38: high: divide by zero (Inspector): check fails here: requires Dens(I) /= 0 (iteration 2 of 3)

In order to decide whether or not to unroll a loop, Inspector uses some heuristics and in particular limits the number of generated statements after unrolling. This limit is 128 statements by default, and can be changed by setting the environment variable CODEPEER_UNROLL_LIMIT to a lower or greater value. Setting this environment variable to 0 will in effect disable loop unrolling. Note that this setting is taken into account when generating SCIL files, so you need to force generation of the SCIL file after enabling it, by e.g. using the GNAT SAS menu GNAT SAS>Advanced>Remove SCIL

14.1.3.5. Calls to Unknown Subprograms

If Inspector encounters calls to subprograms whose body is not available (e.g. imported from another language, or a runtime call not known to Inspector), it will report such calls as part of the info messages.

Unless the --no-presumptions switch is used, Inspector will generate presumptions (see Inspector Presumptions) for each call to unknown subprograms that might need to be reviewed for accuracy. When using --no-presumptions, no such presumptions are generated, at the expense of potentially more false positives.

14.1.3.6. Skipped Subprograms or Files

The static error detection analysis performed by Inspector is space- and time-intensive, because Inspector tracks the value of all program expressions across all possible execution paths. The present implementation of Inspector holds all this information in main memory on the host machine.

Certain Ada programs are too large or too complex to analyze within the memory constraints of the host machine. When Inspector detects this circumstance, it will skip analyzing the subprograms or files which are too complex. No annotations or error messages will be generated for these subprograms or files, and calls to subprograms which were skipped will be analyzed as though the subprogram body were not available. Detailed information about which files were skipped is available in the info messages and may also be found in the low level Inspection.log file when the switch --dbg-on limitations is given.

This kind of limitation is inherent to static analysis, and as for calls to unknown subprograms, calls to these skipped subprograms need to be reviewed manually instead.

As an example, certain syntax parsers can be too complex to analyze within the memory constraints of the host machine. The parsing routine will typically contain a loop and deeply nested switch or if-then-else statements with complex conditional expressions. These can introduce a very large number of paths and distinct computations into the program graph, which may bump into the host machine memory limit.

If you want Inspector to analyze these subprograms, you can try one of the following solutions:

  • increase the number of steps: by default the number of steps for each subprogram is limited to 30 in fast analysis mode, and 1000 in deep analysis mode. You can increase this value by using the --steps switch.

    Note that an exception is made for elaboration procedures, whose analysis is very important in practice. (An analysis failure for an elaboration procedure means that the analysis for the entire module is abandoned.) Consequently, such subprograms are given thrice as many steps as the other ones.

  • increase the amount of memory available on your machine: in case of a memory error reported by Inspector, runing on a machine with more main memory should help analyze complex subprograms.

  • increase the maximum number of values computed: in case Inspector reports a too many value numbers in Inspection.log (when the switch --dbg-on limitations is given), then you might want to use the debug switch --dbg-vn-limit with a value above 100000, e.g: --dbg-vn-limit 150000. If this is not sufficient, you will need to simplify your subprogram, see below.

  • simplify your subprogram: when Inspector reports an inability to analyze a subprogram, it often means that the subprogram is too complex to be analyzed, and should be split into multiple, simpler subprograms which Inspector can analyze separately.

Note that increasing one of the above limits may well run into another limit due to the inherent complexity of these subprograms (e.g. going from a "too many value numbers" limit to a steps limit, to an out of memory limit.

In general our recommendation is to either analyze such complex subprograms manually and let Inspector skip them, or alternatively refactor your code as mentioned above by e.g. splitting it into multiple simpler subprograms.

Since Inspector's analysis is modular, the rest of the code will still be analyzed and produce useful and relevant results.

14.1.3.7. Call Too Complex

When Inspector analyzes a call to a subprogram, it can hit two different kinds of limits. If one of these limits is hit, Inspector will analyze the call as if there were no body available.

If you want Inspector to analyze these calls, you can try to increase the limit. If the switch --dbg-on limitations is given, Inspection.log file contains messages about which limit is hit. By looking for call too complex in a line, you can see two different kinds of limits:

  • in the case of hitting the too many possible call targets limit, it means that there are too many targets for a dispatching call or call through an access-to-subprogram value. You can increase the maximum number of targets with the debug switch --dbg-call-target-limit <limit>.

  • in the case of hitting the too many obj-ids to import limit, you can increase the limit using debug switch --dbg-obj-ids-import-limit <limit>.

14.1.3.8. Dispatching Calls and Access to Subprograms

When Inspector encounters a dispatching call or a call via an access to subprogram variable, Inspector attempts to identify all possible such target calls by considering all the compatible primitive operations (or subprograms whose address was taken and whose profile is compatible in the case of an access to subprogram object). By default, the effect of these calls is taken into account in the caller, but the preconditions for these multiple targets (which may correspond to subprograms that cannot be called at this point) are ignored, to avoid an undue number of false positives. The net effect is that Inspector may not report certain possible precondition failures for multi-target calls.

When --messages max is used, then Inspector checks that the preconditions of all targets are satisfied at the point of call.

14.1.3.9. Null Array Bounds

In most cases, Ada requires that the high and low bounds of an index constraint for an array must belong to the corresponding index subtype of the array type. For example, because Standard.String is declared as:

type String is array (Positive range <>) of Character;

both bounds of an object or value of type String will usually be positive. There is an exception to this rule in the case of an array which has no elements because the low bound is greater than the high bound. In this case, values outside of the index subtype are permitted. It is not at all unusual to see high bounds outside of the index subtype, as in:

X : String(1 .. 0);

and Inspector handles this case. It is much more unusual to see low bounds outside of the index subtype, as in:

Unlikely  : String (-10 .. -20);

It happens that the possibility of an out-of-index-subtype lower bound significantly impairs Inspector's analysis in some cases that do commonly come up in practice. In order to pragmatically deal with this situation, Inspector will, in some cases, treat an out-of-index-subtype lower bound as an error. An array declaration such as the "Unlikely" example above will elaborate without raising any exception, as specified by the Ada language definition. Inspector, however, may treat it as an error. More importantly, in cases where the bounds of array are not known (e.g., a subprogram with a parameter of an unconstrained array subtype such as String), Inspector may assume for purposes of its analysis that the low bound belongs to the index subtype. Given the following example:

procedure P (X : String) is
   First_Is_Positive : Boolean := X'First > 0;
   Last_Is_Positive  : Boolean := X'Last > 0;

Inspector will assume that the local variable First_Is_Positive is initialized to True, whereas it knows that Last_Is_Positive might be False if X'Length = 0.

14.1.3.10. Handling of 'Valid

Inspector interprets X'Valid as X being in the range of its subtype if the declared range is static and if the subtype does not have any subtype predicates. X'Valid is interpreted as an unknown call in other cases.

Given the following example:

procedure P1 (X : Positive) is
begin
   if X'Valid then
      pragma Assert (X > 10);
   end if;
end P1;

Inspector interprets X'Valid as X in 0 .. 2**31-1. Since for an IN [OUT] parameter of a given subtype, Inspector assumes that the parameter is in its statically declared range, the code above will generate a test always true message and a precondition X > 10. Since such messages concerning 'Valid are in general not useful, Inspector suppresses them unless --messages max is used.

Given the following example:

subtype Even is Integer
   with Dynamic_Predicate => Even mod 2 = 0;

procedure P2 (X : Even) is
begin
   if X'Valid then
       pragma Assert (X > 10);
   end if;
end P2;

Inspector interprets X'Valid as an unknown call. It thus does not know anything about its result and cannot generate a precondition for the assertion. This means that Inspector will emit a message about the potentially failing assertion.

Inspector emits a high-ranking message if the prefix of 'Valid is known to be uninitialized. No message is emitted in the case where the prefix of 'Valid only might be uninitialized, unless --messages max is used.

14.1.4. Advanced Inspector Command Line Switches

14.1.4.1. Advanced Inspector Switches

As described in Analyzer package attributes, these switches should be specified with the for Switches ("inspector") attribute of the project file.

  • --additional-patterns <file>

    When present, this file will be merged with MessagePatterns.xml

  • --daemon "<package.subp>"

    Specify that the named subprogram is the entry point of a daemon task (corresponding to an Ada task).

    Subprograms are identified by prefixing the name of the subprogram with the name of the package in which the subprogram is defined. Wildcard matching of the string providing the module and subprogram name (via the asterisk character) is available at the beginning or end of the specification, for example: "*package.subp*".

    See Race Condition Messages for more information.

  • --dbg-vn-limit <num_vns>

    Specify maximum number of "value numbers" allowed during Inspector analysis of a single method/subprogram. See section Skipped Subprograms or Files for more information.

  • --dbg-call-target-limit <limit>

    Specify maximum number of targets for a dispatching call or a call through an access-to-subprogram value. For Inspector analysis only. See section Call Too Complex for more information.

  • --dbg-obj-ids-import-limit <limit>

    Specify maximum number of "obj-ids" to import from a callee to the caller. For Inspector analysis only. See section Call Too Complex for more information.

  • --message-patterns <file>

    Specify an alternate MessagePatterns.xml file.

  • --method-memory-size <megs>

    Specify the maximum memory size in MB to be used by Inspector for analyzing a single method/subprogram. By default, Inspector allocates a maximum of 1/3 of the amount of physical memory on the system to analyze an individual subprogram, to limit memory explosions in some corner cases. For very complex subprograms, this limit may not be sufficient and may be bumped via this switch. See section Skipped Subprograms or Files for more information.

  • --method-timeout <seconds>

    Specify the maximum time spent, in seconds, by Inspector to analyze a single method/subprogram, to avoid spending too much time analyzing very complex subprograms. The default value is set to 600 (10 minutes) in fast mode and 1800 (30 minutes) in deep mode. Note that you should in general use the --steps switch instead.

  • --steps <num>

    Specify the maximum number of steps for allowed before stopping Inspector's analysis of a method/subprogram, to avoid spending too much time analyzing very complex subprograms. The number of steps is calculated deterministically, which means that it will be the same for each run with the same analysis, regardless of the platform's speed. See section Skipped Subprograms or Files for more information.

  • --partition-memory-limit <GB>

    Specify the maximum memory that should be used for each partition of Inspector analysis, in gigabytes. The optional suffix GB may be used explicitly, for example: --partition-memory-limit 8GB or simply: --partition-memory-limit 8

  • --no-preconditions

    Do not propagate checks as implicit preconditions during Inspector analysis. This generally means that code within a subprogram that would be identified as a check will not be promoted as a precondition of the subprogram, typically resulting in messages flagged on lines corresponding to the checks rather than flagged on calls. As an exception to implicit precondition suppression, checks that inputs to subprograms (parameters and referenced globals) are initialized will still be propagated as preconditions, because flagging all uses of inputs would result in too many false-positive messages. In addition, any user-defined preconditions will remain as preconditions and be taken into account on calls. See also --no-propagation below.

  • --no-presumptions

    Instruct Inspector to not generate any presumptions for unanalyzed subprogram calls. Use of this switch will generally result in additional messages being reported in the vicinity of unanalyzed calls, since assumptions about the result of such calls are no longer made.

  • --no-propagation "<package.subp>"

    Instruct Inspector to not propagate checks as implicit preconditions for subprogram "package.subp." This is the same as --no-preconditions, but limited to the named subprogram, which might be a top-level operation for which it is not appropriate to presume the inputs will conform to any non-explicit precondition. The "--no-propagation <package.subp>" switch may appear multiple times, to suppress precondition propagation for more than one subprogram. An asterisk ('*') character may appear at the start or end of the name to indicate a wildcard match.

  • --no-race-conditions

    Suppress analysis and reporting of race conditions in deep analysis mode. This may result in faster analysis.

  • --project-partitions

    Create partitions based on the project hierarchy. See Partitioning of Analysis for more details.

  • --simple-partitions

    Create partitions based on an alphabetical order of package names. See Partitioning of Analysis for more details.

  • --reentrant "<package.subp>"

    Specify that the named subprogram is a reentrant entry point (corresponding to an Ada task type).

    Subprograms are identified by prefixing the name of the subprogram with the name of the package in which the subprogram is defined. Wildcard matching of the string providing the module and subprogram name (via the asterisk character) is available at the beginning or end of the specification, for example: "*package.subp*".

    See Race Condition Messages for more information.

14.1.5. Format of MessagePatterns.xml File

MessagePatterns.xml is a file to control the output of Inspector. Internally, Inspector generates a number of messages, each with a message text and ranking set to Low (see the section about ranking below). With MessagePatterns.xml, one can change the ranking of a message from Inspector or even suppress the message. You can either modify the file that is part of the GNAT SAS installation, or better specify a project-specific file or (preferred when possible) specify an additional project-specific file which comes in addition to the predefined file, see Analyzer package attributes.

Note that a simpler way to filter out messages based on message categories is via the --show command line switch.

14.1.5.1. Structure of the File

The file MessagePatterns.xml is simply a list of rules. The structure is the following:

<?xml version="1.0"?>
<Message_Probability_Rules>
   <!--  the list of rules here -->
</Message_Probability_Rules>

Each rule consists of two parts: a pattern and a target ranking. The pattern describes the type of message that this rule matches. The target ranking is the new ranking of the message as it will appear in the GNAT SAS output. In the XML format, a rule looks like this:

<Message_Rule Matching_Probability="RANKING">
  <Message_Pattern>
     <!-- definition of the pattern -->
  </Message_Pattern>
</Message_Rule>

The target ranking is marked RANKING in this rule, and we will describe below how a pattern can be defined.

The following ranking levels can be used as target ranking, in increasing order:

Info

Info messages about e.g. subprogram not being analyzed.

Low

Low ranking messages.

Medium

Medium ranking messages.

High

High ranking messages.

Suppressed

Used to suppress a message from the reports.

14.1.5.2. Matching of rules

For each message, Inspector will try to match each pattern of each rule. If no pattern matches, the ranking of the message remains Low. If a single pattern matches, the target ranking of the corresponding rule becomes the new ranking of the message. If several rules match, the highest ranking wins. Note in particular that Suppressed being the highest ranking, if a matching rule has target ranking Suppressed, the message is suppressed regardless of other matching rules.

14.1.5.3. Defining patterns

A pattern is a list of specifiers. There are three types of specifiers, Boolean specifiers, String specifiers, and computed specifiers. They are written as follows:

<Bool_Specifier Name_Of_Boolean_Attribute="NAME"></Bool_Specifier>

<String_Specifier
   Name_Of_String_Attribute="NAME"
   String_To_Match="REGEXP">
</String_Specifier>

<Computed_Specifier Name_of_Computed_Attribute="NAME"></Computed_Specifier>

For each type of specifier the NAME can be chosen among a predefined set. In addition, each specifier can be negated using the attribute Complement="YES".

14.1.5.3.1. Boolean specifiers

For Boolean specifiers, the following attributes are allowed:

Attribute Name

Description

Is_Big_Int

The message concerns an integer value.

Is_Big_Rat

The message concerns a real value.

Is_Side_Effect

The message is related to a side effect generated after calling a subprogram.

Is_Pointer

The message concerns a pointer.

Bad_Set_Includes_Null_Literal

The message concerns a pointer value which should not be null.

Expected_Set_Is_Subset_Plus_Minus_1000

The message concerns a value that is in -1000 .. 1000.

Expected_Set_Is_Singleton_Set

The message concerns a value that can have only one concrete value.

Valid_Bad_Set_Is_Singleton_Set

The message concerns a value which can take any concrete values except one.

Valid_Bad_Set_Overlaps_Plus_Minus_1000

The message concerns a value that should not be in -1000 .. 1000.

Precondition_Set_Contains_Holes

The message is about a precondition set which is not a contiguous range.

Precondition_Set_Has_Singleton_Hole_At_Zero

The message is about a precondition set which includes the values around zero, but not zero itself.

Check_Is_Soft

The message is soft (see Inspector Annotations)

Check_Is_Loop_Bound_Check

This message concerns a loop bound check

Bad_Set_Only_Invalid

The message is about a possibly uninitialized memory location.

Message_Depends_On_Unknown_Call

Message depends on unknown call.

Exception_Handler_Present

The enclosing basic block contains an exception handler. This is relevant in the context of validity checks when Inspector is checking whether scalar out parameters have been initialized.

Uninteresting_RHS

Uninteresting RHS in the context of dead store message.

Message_About_Implicit_Call

Message is about implicit (internal) call introduced by the compiler.

Message_Is_Uncertain

The message reflects some uncertainty. Alias for: Message_Depends_On_Unknown_Call or Exception_Handler_Present or Uninteresting_RHS or Message_About_Implicit_Call

Message_Depends_On_Object_Merging

Message depends on approximating more objects with one abstract object. This is the case when, e.g., array others or wildcard dereferences are involved.

Message_Involves_Loop_Imprecision

The message depends on a variable modified in a loop for which Inspector was not able to compute a precise range.

Message_Likely_False_Positive

Approximations were involved when computing information relevant for the message meaning that the message is more likely false positive. Is implied by: Message_Is_Uncertain, Message_Depends_On_Object_Merging, and Message_Involves_Loop_Imprecision

14.1.5.3.2. String specifiers

For String specifiers, the chosen attribute name will influence the meaning of the regular expression that is given with the second attribute. The following table gives an overview over the accepted attribute names.

Attribute Name

Description

Text

Regexp applies to text of the message. See below for details.

Subp

Regexp applies to subprogram name

File

Regexp applies to file name

Directory

Regexp applies to directory name

Likelihood

Regexp applies to likelihood of the failure, allowed values are: CHECK_WILL_FAIL, CHECK_MIGHT_FAIL, CHECK_CANNOT_FAIL

Check_Kind

Regexp applies to the kind of check that is described by the message. See below for a list of possible checks. Note that you can use a regular expression to match several kinds of checks at once. The regexp is not case sensitive for the attribute Check_Kind.

Original_Check_Kind

This is only valid for Precondition_Annotations and Precondition_Checks. Regexp applies to the kind of checks that caused the corresponding precondition.

The regexp above are those used in file names by the Unix shell or DOS prompt:
  • * matches any string of 0 or more characters

  • ? matches any character

  • [char char ...] matches any character listed

  • [char - char] matches any character in given range

  • {elmt, elmt, ...} is an alternation (any of elmt)

A typical Inspector message like

div.adb:7:23: high: divide by zero (Inspector): check fails here; requires I >= 1

starts with the location of the message, followed by its rank, then its kind, and ends with some text with more details. In the case of a regexp with the attribute Text, the regexp applies to the rightmost part of the message which we call text in the following. Depending on the kind of message the regexp might not apply to the whole text. If the text start with requires, like above, the regexp only applies to the text after the requires, here I >= 1. In some cases the kind is not singled out with a colon, as in

main.adb:16:31: medium warning: dead code (Inspector): dead code because (is_equal'Result) = true
test.adb:8:7: medium warning: unused assignment (Inspector): unused assignment into A

In this case the regexp is only applied to the part starting after the kind of the message, here because (is_equal'Result) = true and into A. To get the exact text against which your regexp is applied you can use the debug switch --dbg-on patterns, see below for more information.

The list of Inspector check kind ids in MessagePatterns.xml corresponds mostly to GNAT SAS messages as described in GNAT SAS Messages Reference:

Check Kind

Description

NON_ANALYZED_CALL_WARNING

call not analyzed (due to e.g. tool limitation or code too complex)

SUSPICIOUS_PRECONDITION_WARNING

suspicious precondition

SUSPICIOUS_INPUT_WARNING

suspicious input

SUSPICIOUS_CONSTANT_OPERATION_WARNING

suspicious constant operation

UNREAD_IN_OUT_PARAMETER_WARNING

suspicious in out: could have out mode

UNASSIGNED_IN_OUT_PARAMETER_WARNING

suspicious in out: could have in mode

UNKNOWN_CALL_WARNING

unknown call (due to e.g. partitioning)

DEAD_STORE_WARNING

unused assignment

DEAD_OUTPARAM_STORE_WARNING

unused out parameter

POTENTIALLY_DEAD_STORE_WARNING

unused assignment to global

SAME_VALUE_DEAD_STORE_WARNING

useless reassignment

DEAD_BLOCK_WARNING

dead code

INFINITE_LOOP_WARNING

loop does not complete normally

PLAIN_DEAD_EDGE_WARNING

test predetermined

TRUE_DEAD_EDGE_WARNING

test always true

FALSE_DEAD_EDGE_WARNING

test always false

TRUE_CONDITION_DEAD_EDGE_WARNING

condition predetermined to true

FALSE_CONDITION_DEAD_EDGE_WARNING

condition predetermined to false

DEAD_BLOCK_CONTINUATION_WARNING

continuation of dead code

ANALYZED_MODULE_WARNING

module included in analysis

NON_ANALYZED_MODULE_WARNING

module not included, due to Inspector limitations

NON_ANALYZED_PROCEDURE_WARNING

subprogram not included, due to Inspector limitations

PROCEDURE_DOES_NOT_RETURN_ERROR

subprogram never returns

CHECK_FAILS_ON_EVERY_CALL_ERROR

subprogram always fails

UNLOCKED_REENTRANT_UPDATE_ERROR

unprotected access

UNLOCKED_SHARED_DAEMON_UPDATE_ERROR

unprotected shared access

MISMATCHED_LOCKED_UPDATE_ERROR

mismatched protected access

PRECONDITION_CHECK

precondition

INVALID_OR_NULL_CHECK

access check

DIVIDE_BY_ZERO_CHECK

divide by zero

ARRAY_INDEXING_CHECK

array index check

NUMERIC_OVERFLOW_CHECK

overflow check

USER_ASSIGN_STM_CHECK

overflow check

PRE_ASSIGN_STM_CHECK

overflow check (before a call)

POST_ASSIGN_STM_CHECK

overflow check (after a call)

NUMERIC_RANGE_CHECK

range check

TAG_CHECK

tag check

TYPE_VARIANT_CHECK

discriminant check

ALIASING_CHECK

parameter aliasing

RAISE_CHECK

raise exception

CONDITIONAL_RAISE_CHECK

conditional check

ASSERTION_CHECK

assertion

POSTCONDITION_CHECK

postcondition

INVALID_CHECK

validity check

Note that Inspector issues a warning when a check kind id (or regular expression) does not match any check.

In order to help you come up with correct rules you can use the Inspector debug switch --dbg-on patterns. This switch will make Inspector print all the messages in the following format:

Finding Rule for DEAD_STORE_WARNING: test.adb: test.p: into A

Where the first part is the check kind in the format to be used in a rule, the second part is the file, the third part the procedure containing the message, and the last part the text against which your regexp is matched. The above message is usually printed as:

test.adb:8:7: medium warning: unused assignment (Inspector): unused assignment into A

After each message, the rules that match the corresponding message (if any) are printed, precising the resulting ranking, e.g. :

Finding Rule for DEAD_STORE_WARNING: test.adb: test.p: into A
Rule # 57 matches.
  (first match)
winning probability: MEDIUM

Here you see that rule 57 is the first and only rule that matches the message, and that the resulting ranking is MEDIUM.

14.1.5.3.3. Computed specifiers

For computed specifiers, the allowed set of attribute names is the following:

Attribute Name

Description

Is_Check

The message concerns a check. (see GNAT SAS Messages Reference)

Is_Assign_Stm_Check

The message concerns a check for an assignment.

Is_Assign_Stm_Or_Precond_Check

The message concerns a check for an assignment or a precondition.

Source_Is_Ada

The source for that message is Ada (always true currently).

Message_Is_New

The message is new wrt. the baseline run.

Message_Is_Dropped

The message is in the baseline run, but not this run.

Message_Is_Unchanged

The message is unchanged wrt. the baseline run.

Max_Messages

Switch --messages was given with argument max.

Normal_Messages

Switch --messages was given with argument normal.

Min_Messages

Switch --messages was given with argument min.

14.1.5.4. Example

The following simple example shows a rule which will suppress all messages from the validity check category. Note that a simpler way to achieve the same effect would be to use --be-messages=-validity_check.

<Message_Probability_Rules>
  <Message_Rule Matching_Probability="SUPPRESSED">
    <Message_Pattern>
      <String_Specifier
        Name_Of_String_Attribute="Check_Kind"
        String_To_Match="INVALID_CHECK">
      </String_Specifier>
    </Message_Pattern>
  </Message_Rule>
</Message_Probability_Rules>

The following more complex example shows a rule which will flag as HIGH all check messages which are not soft, will always fail, and which are not user raise statements:

<Message_Probability_Rules>
 <Message_Rule Matching_Probability="HIGH">
   <Message_Pattern>
     <Computed_Specifier Name_Of_Computed_Attribute="Is_Check">
     </Computed_Specifier>

     <Bool_Specifier
       Complemented="YES"
       Name_Of_Boolean_Attribute="CHECK_IS_SOFT">
     </Bool_Specifier>

     <String_Specifier
       Name_Of_String_Attribute="Likelihood"
       String_To_Match="CHECK_WILL_FAIL">
     </String_Specifier>

     <String_Specifier
       Complemented="YES"
       Name_Of_String_Attribute="Check_Kind"
       String_To_Match="RAISE_CHECK">
     </String_Specifier>

   </Message_Pattern>
 </Message_Rule>
</Message_Probability_Rules>

14.2. Inspector By Example

GNAT SAS' Inspector engine is very different from most static analyzers, which execute symbolically the program until a safe approximation is reached. Instead, Inspector works bottom-up from callees to callers, generating a contract for each subprogram along the way. Contracts are the basis for generating error messages related to possible run-time errors and logic errors, and they can be inspected during code reviews to detect further errors. Thus, it is essential to understand how Inspector generates contracts, and how it uses contracts to detect errors. This section aims at providing a deeper insight into how GNAT SAS' Inspector engine works, through a step-by-step exploration of simple code examples. The code for these examples is available in the distribution of GNAT SAS, under <gnatsas install>/share/examples/gnat_sas/inspector_by_example, and in GNAT Studio through menu Help ‣ GNATSAS ‣ Examples ‣ Inspector by Example.

14.2.1. Basic Examples

This section presents the results of running GNAT SAS' Inspector on simple subprograms composed of assignments, branchings and calls.

14.2.1.1. Scalar Assignment

Assign

Inspector is able to follow very precisely the assignments to scalar variables throughout the program. Take a very simple program Assign that assigns the value Y+1 to the variable X:

1procedure Assign (X : out Integer; Y : in Integer) is
2begin
3   X := Y + 1;
4end Assign;

On this subprogram, Inspector generates the following contract:

1assign.adb:1: (pre)- assign:(overflow check) Y /= 2_147_483_647
2assign.adb:1: (post)- assign:X /= -2_147_483_648
3assign.adb:1: (post)- assign:X = Y + 1

The precondition on line 1 requires that the value of Y passed in argument to Assign be less than the maximal integer value. This is good advice indeed, because otherwise the addition on line 3 of Assign overflows.

The postcondition on line 3 indicates that the value of X after the call is equal to Y+1. The postcondition on line 2 indicates that this value is different from (hence greater than) the minimal integer value.

On this first example, Inspector is able to generate the most precise contract for subprogram Assign. On more complex subprograms, Inspector will generate a comprehensive and safe approximation.

Bad_Assign

Consider a version of Assign where X is read inside the subprogram before it is assigned a value:

1procedure Bad_Assign (X : out Integer; Y : in Integer) is
2begin
3   X := X + 1;
4end Bad_Assign;

Inspector detects that X is not initialized when it is read on line 3, and thus it generates an error:

1bad_assign.adb:3:9: high: validity check [CWE 457] (Inspector): X is uninitialized here

Assign_To_Pos

When constrained types are used, for example the type Positive of positive integers in Ada, then Inspector uses this information in the contracts it generates. Take a variation of Assign where parameter X is a positive integer:

1procedure Assign_To_Pos (X : out Positive; Y : in Integer) is
2begin
3   X := Y + 1;
4end Assign_To_Pos;

On this subprogram, Inspector generates the following contract:

1assign_to_pos.adb:1: (pre)- assign_to_pos:(range check) Y in 0..2_147_483_646
2assign_to_pos.adb:1: (post)- assign_to_pos:X = Y + 1
3assign_to_pos.adb:1: (post)- assign_to_pos:X'Initialized

Note how the range of Y in the precondition on line 1 has been restricted to positive integers less than the maximal integer value. Note also that the postcondition on line 3 only indicates now that X is initialized on exit, which is the most precise information here given that the type of X already excludes all negative values. The attribute 'Initialized is not an actual Ada attribute. It is used to denote that a location has been initialized. This is different from the Ada attribute 'Valid which only denotes that a location has a valid bit representation. For example, a variable of a machine integer type is always valid, even if it is not initialized.

Bad_Assign_To_Pos

Consider a version of Assign_To_Pos where the assignment to X always results in a constraint error, whatever the precondition:

1procedure Bad_Assign_To_Pos (X : out Positive; Y : in Positive) is
2begin
3   X := -Y + 1;
4end Bad_Assign_To_Pos;

Inspector detects that no suitable precondition can be generated for this subprogram, and it generates errors:

1bad_assign_to_pos.adb:1:1: high warning: subp always fails (Inspector): bad_assign_to_pos fails for all possible inputs
2bad_assign_to_pos.adb:3:12: high: range check [CWE 682] (Inspector): check fails here

Self_Assign

As shown in the examples above, the precondition refers to values of variables before a call, while the postcondition refers to values of variables after a call. In general, though, postconditions may relate values on entry to a subprogram and values on exit to the same subprogram. Consider the code of Self_Assign which increments the value of its parameter X:

1procedure Self_Assign (X : in out Integer) is
2begin
3   X := X + 1;
4end Self_Assign;

On this subprogram, Inspector generates the following contract:

1self_assign.adb:1: (pre)- self_assign:(overflow check) X /= 2_147_483_647
2self_assign.adb:1: (post)- self_assign:X /= -2_147_483_648
3self_assign.adb:1: (post)- self_assign:X = X'Old + 1

The precondition on line 1 indeed refers to the value of X before the call, and the postcondition on line 2 refers to the value of X after the call. The interesting case is the postcondition on line 3, which relates the value of X before the call, denoted X'Old, and the value of X after the call.

Bad_Self_Assign

Consider a version of Self_Assign where X is assigned its current value previously copied:

1procedure Bad_Self_Assign (X : in out Integer) is
2   Y : Integer := X - 1;
3begin
4   X := Y + 1;
5end Bad_Self_Assign;

When --mode=deep is given, Inspector detects that the assignment is useless, and it generates a warning:

1bad_self_assign.adb:4:6: medium warning: useless reassignment [CWE 563] (Inspector): useless reassignment of X

14.2.1.2. Aggregate Assignment

Assign_Rec

Inspector is able to follow assignments to variables of composite type as precisely as assignments to scalar variables. Take for example a record type Rec aggregating an integer and a Pair, which is itself aggregating two integers:

 1package Assign_Rec is
 2   type Pair is record
 3      A, B : Integer;
 4   end record;
 5   type Rec is record
 6      C : Integer;
 7      D : Pair;
 8   end record;
 9   procedure Assign (X : out Rec; Y : in Integer);
10end Assign_Rec;

The subprogram Assign_Rec.Assign updates some components of its record parameter X:

1package body Assign_Rec is
2   procedure Assign (X : out Rec; Y : in Integer) is
3   begin
4      X.C := Y + 1;
5      X.D.B := Y - 1;
6   end Assign;
7end Assign_Rec;

On this subprogram, Inspector generates the following contract:

1assign_rec.adb:2: (pre)- assign_rec.assign:(overflow check) Y in -2_147_483_647..2_147_483_646
2assign_rec.adb:2: (post)- assign_rec.assign:X.C = Y + 1
3assign_rec.adb:2: (post)- assign_rec.assign:X.C >= -2_147_483_646
4assign_rec.adb:2: (post)- assign_rec.assign:X.D.B <= 2_147_483_645
5assign_rec.adb:2: (post)- assign_rec.assign:X.D.B = Y - 1

The postconditions on lines 2-5 correspond exactly to the most precise postconditions on this subprogram, which one would also get with assignments to scalar variables.

Bad_Assign_Rec

Consider a version of Assign_Rec where Y is multiplied and divided by one million, instead of being incremented and decremented by one:

1with Assign_Rec; use Assign_Rec;
2procedure Bad_Assign_Rec (X : out Rec; Y : in Integer) is
3begin
4   X.C := Y * 1_000_000;
5   X.D.B := Y / 1_000_000;
6end Bad_Assign_Rec;

Inspector detects that the expression Y / 1_000_000 on line 5 always evaluates to zero, and it issues a warning:

1bad_assign_rec.adb:5:15: medium warning: suspicious constant operation (Inspector): operation Y/1_000_000 always evaluates to 0

Indeed, since Y is multiplied by one million, its absolute value should be less than 2147, as indicated by the contract generated by Inspector:

1bad_assign_rec.adb:2: (pre)- bad_assign_rec:(overflow check) Y in -2_147..2_147
2bad_assign_rec.adb:2: (post)- bad_assign_rec:X.C = Y*1_000_000
3bad_assign_rec.adb:2: (post)- bad_assign_rec:X.C in -2_147_000_000..2_147_000_000
4bad_assign_rec.adb:2: (post)- bad_assign_rec:X.D.B = 0

Then, dividing Y by one million always gives the value zero.

Assign_Arr

Similarly, take an array type Arr aggregating ten integers:

1package Assign_Arr is
2   type Arr is array (1 .. 10) of Integer;
3   procedure Assign (X : out Arr; Y : in Integer);
4end Assign_Arr;

The subprogram Assign_Arr.Assign updates some elements of its array parameter X:

1package body Assign_Arr is
2   procedure Assign (X : out Arr; Y : in Integer) is
3   begin
4      X (1) := Y + 1;
5      X (4) := Y - 1;
6   end Assign;
7end Assign_Arr;

On this subprogram, Inspector generates the following contract:

1assign_arr.adb:2: (pre)- assign_arr.assign:(overflow check) Y in -2_147_483_647..2_147_483_646
2assign_arr.adb:2: (post)- assign_arr.assign:X(1 | 4)'Initialized
3assign_arr.adb:2: (post)- assign_arr.assign:X(1) = Y + 1
4assign_arr.adb:2: (post)- assign_arr.assign:X(4) = Y - 1

The postconditions on lines 2-4 correspond exactly to the most precise postconditions already shown for Assign_Rec.Assign.

Bad_Assign_Arr

Consider a version of Assign_Arr where X is assigned at indexes Y and Y + 10:

1with Assign_Arr; use Assign_Arr;
2procedure Bad_Assign_Arr (X : out Arr; Y : in Integer) is
3begin
4   X (Y) := 1;
5   X (Y + 10) := -1;
6end Bad_Assign_Arr;

Inspector detects that the expression Y + 10 on line 5 is outside of the bounds of X, which ranges from 1 to 10, and it issues an error:

1bad_assign_arr.adb:2:1: high warning: subp always fails (Inspector): bad_assign_arr fails for all possible inputs
2bad_assign_arr.adb:4:4: medium: array index check [CWE 120] (Inspector): requires Y in 1..10
3bad_assign_arr.adb:5:4: high: array index check [CWE 120] (Inspector): check fails here; requires Y in -9..0

Indeed, since index Y is used to access X, its value should be between 1 and 10. Then, Y + 10 is greater than the upper bound of X.

Assign_Arr_Unk

Finally, take an array type Arr aggregating an unknown number of integers:

1package Assign_Arr_Unk is
2   type Arr is array (Integer range <>) of Integer;
3   procedure Assign (X : out Arr; Y : in Integer);
4end Assign_Arr_Unk;

The subprogram Assign_Arr_Unk.Assign updates some elements of its array parameter X:

1package body Assign_Arr_Unk is
2   procedure Assign (X : out Arr; Y : in Integer) is
3   begin
4      X (1) := Y + 1;
5      X (4) := Y - 1;
6   end Assign;
7end Assign_Arr_Unk;

On this subprogram, Inspector generates the following contract:

1assign_arr_unk.adb:2: (pre)- assign_arr_unk.assign:(array index check) X'First <= 1
2assign_arr_unk.adb:2: (pre)- assign_arr_unk.assign:(array index check) X'Last >= 4
3assign_arr_unk.adb:2: (pre)- assign_arr_unk.assign:(overflow check) Y in -2_147_483_647..2_147_483_646
4assign_arr_unk.adb:2: (post)- assign_arr_unk.assign:X(1 | 4)'Initialized
5assign_arr_unk.adb:2: (post)- assign_arr_unk.assign:X(1) = Y + 1
6assign_arr_unk.adb:2: (post)- assign_arr_unk.assign:X(4) = Y - 1

The postconditions on lines 4-6 correspond exactly to the most precise postconditions already shown.

14.2.1.3. Branching

Cond_Assign

A common limitation of static analyzers is that they cannot follow precisely disjunctions that occur due to branches in the program. Inspector uses advanced techniques to overcome this difficulty and actually follow these branches. Take a simple subprogram Cond_Assign that assigns a different value to parameter X depending on the value of another parameter B:

1procedure Cond_Assign (X : out Integer; Y : in Integer; B : in Boolean) is
2begin
3   if B then
4      X := Y + 1;
5   else
6      X := Y - 1;
7   end if;
8end Cond_Assign;

On this subprogram, Inspector generates the following contract:

1cond_assign.adb:1: (pre)- cond_assign:(overflow check) B or Y /= -2_147_483_648
2cond_assign.adb:1: (pre)- cond_assign:(overflow check) not B or Y /= 2_147_483_647
3cond_assign.adb:1: (post)- cond_assign:X = One-of{Y + 1, Y - 1}
4cond_assign.adb:1: (post)- cond_assign:X'Initialized

The preconditions on line 1 and 2 precisely represent the conditions that input parameters should respect to avoid an integer negative overflow (on line 6 of Cond_Assign) or positive overflow (on line 4 of Cond_Assign). Note how these preconditions are expressed as disjunctions. This is because the negative or positive overflow is only possible for a particular value of B (which differs between the negative overflow and the positive overflow).

The postcondition on line 3 indicates that the value of X on exit either comes from the assignment on line 4, or the one on line 6. This is not the most precise postcondition, as one could state that, on exit:

(B = True and X = Y + 1) or (B = False and X = Y - 1)

As seen with the Call_Assign example below, Inspector actually generates internally this more precise postcondition, from which it outputs on line 3 a more user-friendly postcondition. The internal (more precise) postcondition is used for analyzing callers of Cond_Assign.

Bad_Cond_Assign

Consider a version of Cond_Assign with a modified test:

1procedure Bad_Cond_Assign (X : out Integer; Y : in Integer; B : in Boolean) is
2begin
3   if B or not B then
4      X := Y + 1;
5   else
6      X := Y - 1;
7   end if;
8end Bad_Cond_Assign;

Inspector detects that the test on line 3 is always true and it issues a warning:

1bad_cond_assign.adb:3:9: medium warning: test always true [CWE 571] (Inspector): test always true because B or not B
2bad_cond_assign.adb:6:9: medium warning: dead code [CWE 561] (Inspector): dead code because B or not B

Multi_Cond_Assign

The precise analysis of branches seen on Cond_Assign is performed by Inspector on any number of if statements, as shown on Multi_Cond_Assign:

 1procedure Multi_Cond_Assign (X : out Integer; Y : in Integer; B1, B2 : in Boolean) is
 2begin
 3   if B1 then
 4      X := Y + 1;
 5   else
 6      if B2 then
 7	 X := Y - 1;
 8      else
 9	 X := Y * 2;
10      end if;
11   end if;
12end Multi_Cond_Assign;

On this subprogram, Inspector generates the following contract:

1multi_cond_assign.adb:1: (pre)- multi_cond_assign:(overflow check) B1 or B2 or Y*2 in -2_147_483_648..2_147_483_647
2multi_cond_assign.adb:1: (pre)- multi_cond_assign:(overflow check) B1 or not B2 or Y /= -2_147_483_648
3multi_cond_assign.adb:1: (pre)- multi_cond_assign:(overflow check) not B1 or Y /= 2_147_483_647
4multi_cond_assign.adb:1: (post)- multi_cond_assign:X = One-of{Y + 1, Y - 1, Y*2}
5multi_cond_assign.adb:1: (post)- multi_cond_assign:X'Initialized

The postcondition on line 4 is similar to the one obtained for Cond_Assign, except here it deals with two if statements. Our comment about Inspector generating internally a contract more precise than the one displayed for Cond_Assign is also applicable here.

Bad_Multi_Cond_Assign

Consider a version of Multi_Cond_Assign with modified tests:

 1procedure Bad_Multi_Cond_Assign (X : out Integer; Y : in Integer; B1, B2 : in Boolean) is
 2begin
 3   if B1 or Y < 10 then
 4      X := Y + 1;
 5   else
 6      if B2 and not (Y > 0) then
 7	 X := Y - 1;
 8      else
 9	 X := Y * 2;
10      end if;
11   end if;
12end Bad_Multi_Cond_Assign;

Inspector detects that the test on line 6 is always false and it issues a warning:

1bad_multi_cond_assign.adb:6:13: low warning: test always false [CWE 570] (Inspector): test always false because not B2 or Y >= 1
2bad_multi_cond_assign.adb:7:12: medium warning: dead code [CWE 561] (Inspector): dead code because not B2 or Y >= 1

Case_Assign

The analysis of case statements is similar to the analysis of if statements. Consider the subprogram Case_Assign.Assign which takes an operation Op in parameter, among three possibilities:

1package Case_Assign is
2  type Oper is (Incr, Decr, Double);
3  procedure Assign (X : out Integer; Y : in Integer; Op : in Oper);
4end Case_Assign;

It does the obvious assignment in each case:

 1package body Case_Assign is
 2  procedure Assign (X : out Integer; Y : in Integer; Op : in Oper) is
 3  begin
 4     case Op is
 5        when Incr =>
 6           X := Y + 1;
 7        when Decr =>
 8           X := Y - 1;
 9        when Double =>
10           X := Y * 2;
11     end case;
12  end Assign;
13end Case_Assign;

On this subprogram, Inspector generates the following contract:

1case_assign.adb:2: (pre)- case_assign.assign:(overflow check) Op /= Decr or Y /= -2_147_483_648
2case_assign.adb:2: (pre)- case_assign.assign:(overflow check) Op /= Double or Y*2 in -2_147_483_648..2_147_483_647
3case_assign.adb:2: (pre)- case_assign.assign:(overflow check) Op /= Incr or Y /= 2_147_483_647
4case_assign.adb:2: (post)- case_assign.assign:X = One-of{Y + 1, Y - 1, Y*2}
5case_assign.adb:2: (post)- case_assign.assign:X'Initialized

The postcondition on line 4 is the same as what is obtained for if statements in Multi_Cond_Assign. The same comment about the internally more precise contract still applies.

14.2.1.4. Call Statement

Call_Assign

The modular treatment of calls is a cornerstone of Inspector analysis. Before a caller of subprogram S is analyzed, the body of S is analyzed and Inspector generates a contract for S. Then, this contract is used to completely abstract the calls to S during the analysis of its callers. In fast mode, Inspector uses precise contracts for all the calls in the same library unit and treats other as unknown. In deep mode, Inspector uses precise contracts for all the calls in the same partition - see Partitioning of Analysis for more details. Direct and mutually recursive calls require that a coarser contract is used for bootstrapping. This coarser contract is then iteratively refined. Consider the subprogram Call_Assign.Call:

1package Call_Assign is
2   type Choice is (Simple, Conditional, Self);
3   procedure Call (X : in out Integer; C : in Choice);
4end Call_Assign;

It calls one of Assign, Cond_Assign or Self_Assign seen previously, depending on the value of its parameter C:

 1with Assign, Cond_Assign, Self_Assign;
 2package body Call_Assign is
 3   procedure Call (X : in out Integer; C : in Choice) is
 4   begin
 5      case C is
 6         when Simple =>
 7            Assign (X, 1);
 8         when Conditional =>
 9            Cond_Assign (X, 2, True);
10         when Self =>
11            Self_Assign (X);
12      end case;
13   end Call;
14end Call_Assign;

On this subprogram, when run with --mode=deep, Inspector generates the following contract:

1call_assign.adb:3: (pre)- call_assign.call:(overflow check) C /= Self or X /= 2_147_483_647
2call_assign.adb:3: (post)- call_assign.call:X /= -2_147_483_648
3call_assign.adb:3: (post)- call_assign.call:X = One-of{2, 3, X'Old + 1}

The precondition on line 1 only constrains the input value of X when C is equal to Self. Indeed, the generated preconditions for Assign and Cond_Assign are always satisfied by the values passed in parameters in Call_Assign, while the generated precondition for Self_Assign imposes that X is less than the maximal integer value.

The postcondition on line 3 enumerates the possible values for X on exit. The value 2 corresponds to the call to Assign. The value 3 corresponds to the call to Cond_Assign. Note here that Inspector used the more precise internal contract for Cond_Assign instead of the one displayed, which would have lead to an additional possible value of 1 for X. The value X'Old + 1 corresponds to the call to Self_Assign.

The postcondition on line 2 is a common constraint on X on exit, whatever the procedure called.

Bad_Call_Assign

Consider a version of Call_Assign in which subprograms Assign, Cond_Assign and Self_Assign are all called in contexts that violate their generated preconditions:

 1with Call_Assign; use Call_Assign;
 2with Assign, Cond_Assign, Self_Assign;
 3procedure Bad_Call_Assign (X : in out Integer; C : in Choice) is
 4begin
 5   case C is
 6      when Simple =>
 7	 Assign (X, Integer'Last);
 8      when Conditional =>
 9	 Cond_Assign (X, Integer'Last, True);
10      when Self =>
11	 X := Integer'Last;
12	 Self_Assign (X);
13   end case;
14end Bad_Call_Assign;

Inspector detects these violations and it issues corresponding precondition errors:

1bad_call_assign.adb:3:1: medium warning: unread parameter (Inspector): parameter X could have mode out

Reverse_Call_Assign

You might be wondering whether Inspector computed a more precise internal postcondition for the postcondition of Call_Assign displayed on line 3. This is indeed the case, as shown in the following example. Consider three subprograms for each value of the Choice enumeration:

1package Reverse_Call_Assign is
2   procedure Call_Simple (X : in out Integer);
3   procedure Call_Conditional (X : in out Integer);
4   procedure Call_Self (X : in out Integer);
5end Reverse_Call_Assign;

Each subprogram calls Call_Assign.Call with the corresponding value for parameter C:

 1with Call_Assign; use Call_Assign;
 2package body Reverse_Call_Assign is
 3   procedure Call_Simple (X : in out Integer) is
 4   begin
 5      Call (X, Simple);
 6   end Call_Simple;
 7   procedure Call_Conditional (X : in out Integer) is
 8   begin
 9      Call (X, Conditional);
10   end Call_Conditional;
11   procedure Call_Self (X : in out Integer) is
12   begin
13      Call (X, Self);
14   end Call_Self;
15end Reverse_Call_Assign;

On these subprograms, when run with --mode=deep, Inspector generates the following contracts:

1reverse_call_assign.adb:3: (post)- reverse_call_assign.call_simple:X = 2
2reverse_call_assign.adb:7: (post)- reverse_call_assign.call_conditional:X = 3
3reverse_call_assign.adb:11: (pre)- reverse_call_assign.call_self:(overflow check) X /= 2_147_483_647
4reverse_call_assign.adb:11: (post)- reverse_call_assign.call_self:X /= -2_147_483_648
5reverse_call_assign.adb:11: (post)- reverse_call_assign.call_self:X = X'Old + 1

The postcondition on line 1 is indeed the most precise contract on Call_Simple, as it is the same as the contract generated for Assign with the value of 1 for Y.

The postcondition on line 2 is indeed the most precise contract on Call_Conditional, as it is the same as the contract generated for Cond_Assign with the value of 2 for Y and True for B.

The precondition on line 3 and the postconditions on lines 4 and 5 are indeed the most precise contract on Call_Self, as they are the same as the contract generated for Self_Assign.

Inspector could only generate these most precise contracts because it did not lose the relation between the input value of parameter C and the output value of parameter X in Call_Assign.Call. The internal most precise postcondition computed by Inspector for Call_Assign.Call is really:

(C = Simple and X = 2) or (C = Conditional and X = 3) or (C = Self and X = X'Old + 1)

Top_Down

Inspector completely works bottom-up on the call-graph, propagating information gathered from callees in their generated contracts to their callers. This strategy ensures that partial applications can be handled easily, and that the analysis is much faster than the classical top-down approach. Although Inspector does not use knowledge of the calling context when analyzing a subprogram, in many cases it generates contracts precise enough to take this context into account.

Consider the local function Ident which is always called in a context where the content of array A is all ones:

 1with Assign_Arr; use Assign_Arr;
 2procedure Top_Down (Y : out Integer) is
 3   A : Arr;
 4   function Ident (X : Integer) return Integer is
 5   begin
 6      if A (X) > 0 then
 7         return X;
 8      else
 9         return 0;
10      end if;
11   end Ident;
12begin
13   A := (others => 1);
14   Y := A (Ident (3));
15end Top_Down;

Inspector does not use this information in the analysis of Ident, but instead generates a conditional postcondition taking the value of X into account (even though this is not visible in the printable version, internally Inspector knows that One-of{X, 0} really means X when A(X) > 0 and 0 otherwise):

1top_down.adb:2: (post)- top_down:Y = 1
2top_down.adb:4: (pre)- top_down.ident:(validity check) A(X)'Initialized
3top_down.adb:4: (pre)- top_down.ident:(array index check) X in 1..10
4top_down.adb:4: (post)- top_down.ident:top_down.ident'Result = One-of{X, 0}
5top_down.adb:4: (post)- top_down.ident:top_down.ident'Result in 0..10

Thus, Inspector can then check that the call to Ident(3) returns a positive value since at this point it knows that X(3) > 0.

14.2.2. Loop Examples

This section presents the results of running GNAT SAS' Inspector on subprograms containing loops.

14.2.2.1. Induction Variables

Induction

The first difficulty in analyzing loops is that it is not possible in general to completely unroll the loop, either because the number of iterations is very large, or because it is not known statically. Thus, the static analysis must analyze together all iterations beyond a certain point. This is a source of approximations, in particular for induction variables, that is, those variables assigned in the loop. However, Inspector analyzes precisely those induction variables that are incremented or decremented at each run through the loop.

For example, take a simple subprogram incrementing a variable X 10 times:

1procedure Induction (X : out Integer) is
2begin
3   X := 0;
4   for J in 1 .. 10 loop
5      X := X + 1;
6   end loop;
7end Induction;

On this subprogram, Inspector generates the following contract:

1induction.adb:1: (post)- induction:X = 10

Inspector manages here to generate the most precise postcondition for the loop.

Bad_Induction

Consider a version of Induction in which X is not initialized before the loop:

1procedure Bad_Induction (X : out Integer) is
2begin
3   for J in 1 .. 10 loop
4      X := X + 1;
5   end loop;
6end Bad_Induction;

Inspector detects that X might be uninitialized on line 4 and it issues an error:

1bad_induction.adb:4:12: high: validity check [CWE 457] (Inspector): check fails here; requires X'Initialized (iteration 1 of 10)

Multi_Induction

In many cases, Inspector can also follow precisely the relations between induction variables in a loop, as in subprogram Multi_Induction:

1procedure Multi_Induction (X1, X2 : out Integer; Y : in Integer) is
2begin
3   X1 := 0;
4   X2 := 0;
5   while X1 < Y loop
6      X1 := X1 + 2;
7      X2 := X2 + 6;
8   end loop;
9end Multi_Induction;

On this subprogram, Inspector generates the following contract:

1multi_induction.adb:1: (pre)- multi_induction:Y <= 715_827_882
2multi_induction.adb:1: (post)- multi_induction:X1 - Y in 0..2_147_483_648
3multi_induction.adb:1: (post)- multi_induction:X1 in (0 | 2 | 4 | 6 | ... | 44..715_827_882)
4multi_induction.adb:1: (post)- multi_induction:X2 - Y*3 in -2_147_483_646..6_442_450_944
5multi_induction.adb:1: (post)- multi_induction:X2 = X1*3
6multi_induction.adb:1: (post)- multi_induction:X2 in (0 | 6 | 12 | 18 | ... | 132..2_147_483_646)

Note how the postcondition on line 5 correctly indicates the relation between X1 and X2 after the loop.

Bad_Multi_Induction

Consider a version of Multi_Induction in which X1 is not assigned in the loop:

1procedure Bad_Multi_Induction (X1, X2 : out Integer; Y : in Positive) is
2begin
3   X1 := 0;
4   X2 := 0;
5   while X1 < Y loop
6      X2 := X2 + 6;
7   end loop;
8   return;
9end Bad_Multi_Induction;

Inspector detects that the loop never terminates, because the exit test on line 5 is always true:

1bad_multi_induction.adb:1:1: medium warning: subp never returns (Inspector): bad_multi_induction never returns
2bad_multi_induction.adb:5:13: medium warning: test always true [CWE 571] (Inspector): test always true because X1 < Y
3bad_multi_induction.adb:5:13: medium warning: loop does not complete normally [CWE 835] (Inspector)
4bad_multi_induction.adb:8:4: medium warning: dead code [CWE 561] (Inspector): dead code because X1 < Y

14.2.2.2. Array Scanning

The most frequent use of loops is to iterate over a collection, for example an array. Use of arrays introduces a new difficulty, namely that the static analysis must handle a large or unbounded number of memory locations. This requires performing some abstractions, to compute a safe approximation.

Search

Take a simple function Search scanning an array for a value. We are reusing here the type Assign_Arr.Arr from example Assign_Arr of arrays of ten integers.

 1with Assign_Arr; use Assign_Arr;
 2function Search (X : in Arr; Y : in Integer) return Integer is
 3begin
 4   for J in X'Range loop
 5      if X (J) = Y then
 6         return J;
 7      end if;
 8   end loop;
 9   return 0;
10end Search;

On this subprogram, Inspector generates the following contract:

 1search.adb:2: (pre)- search:(validity check) X(1) = Y or X(2) = Y or X(3) = Y or X(4) = Y or X(5) = Y or X(6) = Y or X(7) = Y or X(8) = Y or X(9) = Y or X(10)'Initialized
 2search.adb:2: (pre)- search:(validity check) X(1) = Y or X(2) = Y or X(3) = Y or X(4) = Y or X(5) = Y or X(6) = Y or X(7) = Y or X(8) = Y or X(9)'Initialized
 3search.adb:2: (pre)- search:(validity check) X(1) = Y or X(2) = Y or X(3) = Y or X(4) = Y or X(5) = Y or X(6) = Y or X(7) = Y or X(8)'Initialized
 4search.adb:2: (pre)- search:(validity check) X(1) = Y or X(2) = Y or X(3) = Y or X(4) = Y or X(5) = Y or X(6) = Y or X(7)'Initialized
 5search.adb:2: (pre)- search:(validity check) X(1) = Y or X(2) = Y or X(3) = Y or X(4) = Y or X(5) = Y or X(6)'Initialized
 6search.adb:2: (pre)- search:(validity check) X(1) = Y or X(2) = Y or X(3) = Y or X(4) = Y or X(5)'Initialized
 7search.adb:2: (pre)- search:(validity check) X(1) = Y or X(2) = Y or X(3) = Y or X(4)'Initialized
 8search.adb:2: (pre)- search:(validity check) X(1) = Y or X(2) = Y or X(3)'Initialized
 9search.adb:2: (pre)- search:(validity check) X(1) = Y or X(2)'Initialized
10search.adb:2: (pre)- search:(validity check) X(1)'Initialized
11search.adb:2: (post)- search:search'Result in 0..10

The preconditions on lines 1-10 exactly capture in which case the function can be called, regarding the initialization of the array X. Indeed, it is safe to call Assign_arr with a partially initialized array, provided the value Y is found before reaching the uninitialized part.

The postcondition on line 11 is a safe approximation on Search. The most precise postcondition would have to state that either the array X does not contain Y, in which case Search returns 0, or the array X contains Y, in which case Search returns the smallest corresponding index.

Bad_Search

Consider a version of Search in which the final return statement is missing:

1with Assign_Arr; use Assign_Arr;
2function Bad_Search (X : in Arr; Y : in Integer) return Integer is
3begin
4   for J in X'Range loop
5      if X (J) = Y then
6         return J;
7      end if;
8   end loop;
9end Bad_Search;

On this subprogram, Inspector generates the following contract:

 1bad_search.adb:2: (pre)- bad_search:(conditional raise) X(1) = Y or X(2) = Y or X(3) = Y or X(4) = Y or X(5) = Y or X(6) = Y or X(7) = Y or X(8) = Y or X(9) = Y or X(10) = Y
 2bad_search.adb:2: (pre)- bad_search:(validity check) X(1) = Y or X(2) = Y or X(3) = Y or X(4) = Y or X(5) = Y or X(6) = Y or X(7) = Y or X(8) = Y or X(9) = Y or X(10)'Initialized
 3bad_search.adb:2: (pre)- bad_search:(validity check) X(1) = Y or X(2) = Y or X(3) = Y or X(4) = Y or X(5) = Y or X(6) = Y or X(7) = Y or X(8) = Y or X(9)'Initialized
 4bad_search.adb:2: (pre)- bad_search:(validity check) X(1) = Y or X(2) = Y or X(3) = Y or X(4) = Y or X(5) = Y or X(6) = Y or X(7) = Y or X(8)'Initialized
 5bad_search.adb:2: (pre)- bad_search:(validity check) X(1) = Y or X(2) = Y or X(3) = Y or X(4) = Y or X(5) = Y or X(6) = Y or X(7)'Initialized
 6bad_search.adb:2: (pre)- bad_search:(validity check) X(1) = Y or X(2) = Y or X(3) = Y or X(4) = Y or X(5) = Y or X(6)'Initialized
 7bad_search.adb:2: (pre)- bad_search:(validity check) X(1) = Y or X(2) = Y or X(3) = Y or X(4) = Y or X(5)'Initialized
 8bad_search.adb:2: (pre)- bad_search:(validity check) X(1) = Y or X(2) = Y or X(3) = Y or X(4)'Initialized
 9bad_search.adb:2: (pre)- bad_search:(validity check) X(1) = Y or X(2) = Y or X(3)'Initialized
10bad_search.adb:2: (pre)- bad_search:(validity check) X(1) = Y or X(2)'Initialized
11bad_search.adb:2: (pre)- bad_search:(validity check) X(1)'Initialized
12bad_search.adb:2: (post)- bad_search:bad_search'Result in 1..10

Inspector will figure out that in order for the subprogram to exit successfully, the loop should never terminate without a return, otherwise the subprogram would raise Program_Error (as mandated by the Ada standard). This is expressed by the precondition at line 1.

Call_Search

Inspector also computes the set of inputs and outputs of Search, whether these are reachable from parameters or global variables. These inputs and outputs form a part of the generated contract not shown here. Now consider a caller of Search, that calls it twice on the same inputs:

1with Assign_Arr; use Assign_Arr;
2with Search;
3procedure Call_Search (X : in Arr; Y : in Integer; U, V : out Integer) is
4begin
5   U := Search (X, Y);
6   V := Search (X, Y);
7end Call_Search;

On this subprogram, when run with --mode=deep, Inspector generates the following contract:

 1call_search.adb:3: (pre)- call_search:(validity check) X(1) = Y or X(2) = Y or X(3) = Y or X(4) = Y or X(5) = Y or X(6) = Y or X(7) = Y or X(8) = Y or X(9) = Y or X(10)'Initialized
 2call_search.adb:3: (pre)- call_search:(validity check) X(1) = Y or X(2) = Y or X(3) = Y or X(4) = Y or X(5) = Y or X(6) = Y or X(7) = Y or X(8) = Y or X(9)'Initialized
 3call_search.adb:3: (pre)- call_search:(validity check) X(1) = Y or X(2) = Y or X(3) = Y or X(4) = Y or X(5) = Y or X(6) = Y or X(7) = Y or X(8)'Initialized
 4call_search.adb:3: (pre)- call_search:(validity check) X(1) = Y or X(2) = Y or X(3) = Y or X(4) = Y or X(5) = Y or X(6) = Y or X(7)'Initialized
 5call_search.adb:3: (pre)- call_search:(validity check) X(1) = Y or X(2) = Y or X(3) = Y or X(4) = Y or X(5) = Y or X(6)'Initialized
 6call_search.adb:3: (pre)- call_search:(validity check) X(1) = Y or X(2) = Y or X(3) = Y or X(4) = Y or X(5)'Initialized
 7call_search.adb:3: (pre)- call_search:(validity check) X(1) = Y or X(2) = Y or X(3) = Y or X(4)'Initialized
 8call_search.adb:3: (pre)- call_search:(validity check) X(1) = Y or X(2) = Y or X(3)'Initialized
 9call_search.adb:3: (pre)- call_search:(validity check) X(1) = Y or X(2)'Initialized
10call_search.adb:3: (pre)- call_search:(validity check) X(1)'Initialized
11call_search.adb:3: (post)- call_search:U in 0..10
12call_search.adb:3: (post)- call_search:V = U

Inspector uses the knowledge that the same inputs produce the same outputs to compute the postcondition on line 12.

Bad_Call_Search

Consider a caller of Search which does not initialize the last element of parameter X before the call:

 1with Assign_Arr; use Assign_Arr;
 2with Search;
 3function Bad_Call_Search return Integer is
 4   X : Arr;
 5begin
 6   for J in 1 .. 9 loop
 7      X (J) := J;
 8   end loop;
 9   return Search (X, 10);
10end Bad_Call_Search;

When run with --mode=deep, Inspector generates an error, as it understands that X(10) is not initialized, and that 10 is not among the possible values at indices 1..9.

1bad_call_search.adb:3:1: high warning: subp always fails (Inspector): bad_call_search fails for all possible inputs
2bad_call_search.adb:9:11: high: precondition <validity check> [CWE 457] (Inspector): precondition failure on call to search; requires X(1) = Y or X(2) = Y or X(3) = Y or X(4) = Y or X(5) = Y or X(6) = Y or X(7) = Y or X(8) = Y or X(9) = Y or X(10) to be initialized

Search_Unk

Take now a slightly different function Search_Unk. It also scans an array for a value, but we are reusing here the type Assign_Arr_Unk.Arr from example Assign_Arr_Unk of unconstrained arrays, which may contain an unbounded number of integers.

 1with Assign_Arr_Unk; use Assign_Arr_Unk;
 2function Search_Unk (X : in Arr; Y : in Integer) return Integer is
 3begin
 4   for J in X'Range loop
 5      if X (J) = Y then
 6         return J;
 7      end if;
 8   end loop;
 9   return 0;
10end Search_Unk;

On this subprogram, Inspector generates the following contract:

1search_unk.adb:2: (pre)- search_unk:X'Length = 0 or X(J)'Initialized
2search_unk.adb:2: (pre)- search_unk:(validity check) X'Length = 0 or X(X'First)'Initialized
3search_unk.adb:2: (post)- search_unk:search_unk'Result'Initialized

Inspector generates a precondition that array X is completely initialized upon entry to Search_Unk. However, this is too strong for the function to execute safely: it would be sufficient to have X(X'First) initialized, and equal to Y.

Search_While

The same reasoning as above applies to other forms of loops. Suppose we rewrite Search using a while loop instead of a for loop:

 1with Assign_Arr_Unk; use Assign_Arr_Unk;
 2function Search_While (X : in Arr; Y : in Integer) return Integer is
 3   J : Integer := X'First;
 4begin
 5   while J <= X'Last loop
 6      if X (J) = Y then
 7         return J;
 8      end if;
 9      J := J + 1;
10   end loop;
11   return 0;
12end Search_While;

On this subprogram, Inspector generates the following contract:

1search_while.adb:2: (pre)- search_while:(overflow check) X'Length = 0 or X(J)'Initialized
2search_while.adb:2: (pre)- search_while:(validity check) X'Length = 0 or X(X'First)'Initialized
3search_while.adb:2: (pre)- search_while:(overflow check) X(X'First) = Y or X'Length = 0 or X'First /= 2_147_483_647
4search_while.adb:2: (post)- search_while:search_while'Result'Initialized

Here we have one additional precondition that worries about the possibility of overflow on adding one to J, which is not an issue for "for" loops, but otherwise, we have the same contract as the one for Search_Unk.

Search_Loop

Now, we rewrite Search using an indefinite loop instead of a for loop:

 1with Assign_Arr_Unk; use Assign_Arr_Unk;
 2function Search_Loop (X : in Arr; Y : in Integer) return Integer is
 3   J : Integer := X'First;
 4begin
 5   loop
 6      if J > X'Last then
 7         return 0;
 8      end if;
 9      if X (J) = Y then
10         return J;
11      end if;
12      J := J + 1;
13   end loop;
14end Search_Loop;

On this subprogram, Inspector generates the following contract:

1search_loop.adb:2: (pre)- search_loop:(overflow check) X'Length = 0 or X(J)'Initialized
2search_loop.adb:2: (pre)- search_loop:(validity check) X'Length = 0 or X(X'First)'Initialized
3search_loop.adb:2: (pre)- search_loop:(overflow check) X(X'First) = Y or X'Length = 0 or X'First /= 2_147_483_647
4search_loop.adb:2: (post)- search_loop:search_loop'Result'Initialized

Again, this is the same contract as the one for Search_While.

14.2.2.3. Array Increments

Assign_All_Arr

Let's consider now subprograms that iterate over arrays to increment their elements. The procedure Assign_All_Arr increments each element of its array parameter X:

1with Assign_Arr; use Assign_Arr;
2procedure Assign_All_Arr (X : in out Arr) is
3begin
4   for J in X'Range loop
5      X (J) := X (J) + 1;
6   end loop;
7end Assign_All_Arr;

On this subprogram, Inspector generates the following contract:

 1assign_all_arr.adb:2: (pre)- assign_all_arr:(overflow check) X(1) /= 2_147_483_647
 2assign_all_arr.adb:2: (pre)- assign_all_arr:(overflow check) X(10) /= 2_147_483_647
 3assign_all_arr.adb:2: (pre)- assign_all_arr:(overflow check) X(2) /= 2_147_483_647
 4assign_all_arr.adb:2: (pre)- assign_all_arr:(overflow check) X(3) /= 2_147_483_647
 5assign_all_arr.adb:2: (pre)- assign_all_arr:(overflow check) X(4) /= 2_147_483_647
 6assign_all_arr.adb:2: (pre)- assign_all_arr:(overflow check) X(5) /= 2_147_483_647
 7assign_all_arr.adb:2: (pre)- assign_all_arr:(overflow check) X(6) /= 2_147_483_647
 8assign_all_arr.adb:2: (pre)- assign_all_arr:(overflow check) X(7) /= 2_147_483_647
 9assign_all_arr.adb:2: (pre)- assign_all_arr:(overflow check) X(8) /= 2_147_483_647
10assign_all_arr.adb:2: (pre)- assign_all_arr:(overflow check) X(9) /= 2_147_483_647
11assign_all_arr.adb:2: (post)- assign_all_arr:X(1) = X(1)'Old + 1
12assign_all_arr.adb:2: (post)- assign_all_arr:X(1..10) /= -2_147_483_648
13assign_all_arr.adb:2: (post)- assign_all_arr:X(10) = X(10)'Old + 1
14assign_all_arr.adb:2: (post)- assign_all_arr:X(2) = X(2)'Old + 1
15assign_all_arr.adb:2: (post)- assign_all_arr:X(3) = X(3)'Old + 1
16assign_all_arr.adb:2: (post)- assign_all_arr:X(4) = X(4)'Old + 1
17assign_all_arr.adb:2: (post)- assign_all_arr:X(5) = X(5)'Old + 1
18assign_all_arr.adb:2: (post)- assign_all_arr:X(6) = X(6)'Old + 1
19assign_all_arr.adb:2: (post)- assign_all_arr:X(7) = X(7)'Old + 1
20assign_all_arr.adb:2: (post)- assign_all_arr:X(8) = X(8)'Old + 1
21assign_all_arr.adb:2: (post)- assign_all_arr:X(9) = X(9)'Old + 1

The preconditions on lines 1 to 10 ensure that all additions on line 5 of Assign_All_Arr will not overflow. These preconditions are really the most precise ones for subprogram Assign_All_Arr, guaranteeing that no run-time error can be raised during subprogram execution.

Bad_Assign_All_Arr

Consider a version of Assign_All_Arr in which elements are shifted to the left of the array:

1with Assign_Arr; use Assign_Arr;
2procedure Bad_Assign_All_Arr (X : in out Arr) is
3begin
4   for J in X'Range loop
5      X (J) := X (J + 1);
6   end loop;
7end Bad_Assign_All_Arr;

Inspector generates an index error because the last iteration of the loop will access past the upper bound of X:

1bad_assign_all_arr.adb:2:1: high warning: subp always fails (Inspector): bad_assign_all_arr fails for all possible inputs
2bad_assign_all_arr.adb:5:16: high: array index check [CWE 120] (Inspector): check fails here; requires 10 + 1 <= 10 (iteration 10 of 10)

Assign_All_Arr_Incr

Take now a slight variation over example Assign_All_Arr that increments each element by a different amount:

1with Assign_Arr; use Assign_Arr;
2procedure Assign_All_Arr_Incr (X : in out Arr) is
3begin
4   for J in X'Range loop
5      X (J) := X (J) + J;
6   end loop;
7end Assign_All_Arr_Incr;

On this subprogram, Inspector generates the following contract:

 1assign_all_arr_incr.adb:2: (pre)- assign_all_arr_incr:(overflow check) X(1) /= 2_147_483_647
 2assign_all_arr_incr.adb:2: (pre)- assign_all_arr_incr:(overflow check) X(10) <= 2_147_483_637
 3assign_all_arr_incr.adb:2: (pre)- assign_all_arr_incr:(overflow check) X(2) <= 2_147_483_645
 4assign_all_arr_incr.adb:2: (pre)- assign_all_arr_incr:(overflow check) X(3) <= 2_147_483_644
 5assign_all_arr_incr.adb:2: (pre)- assign_all_arr_incr:(overflow check) X(4) <= 2_147_483_643
 6assign_all_arr_incr.adb:2: (pre)- assign_all_arr_incr:(overflow check) X(5) <= 2_147_483_642
 7assign_all_arr_incr.adb:2: (pre)- assign_all_arr_incr:(overflow check) X(6) <= 2_147_483_641
 8assign_all_arr_incr.adb:2: (pre)- assign_all_arr_incr:(overflow check) X(7) <= 2_147_483_640
 9assign_all_arr_incr.adb:2: (pre)- assign_all_arr_incr:(overflow check) X(8) <= 2_147_483_639
10assign_all_arr_incr.adb:2: (pre)- assign_all_arr_incr:(overflow check) X(9) <= 2_147_483_638
11assign_all_arr_incr.adb:2: (post)- assign_all_arr_incr:X(1) = X(1)'Old + 1
12assign_all_arr_incr.adb:2: (post)- assign_all_arr_incr:X(1..10) /= -2_147_483_648
13assign_all_arr_incr.adb:2: (post)- assign_all_arr_incr:X(10) = X(10)'Old + 10
14assign_all_arr_incr.adb:2: (post)- assign_all_arr_incr:X(2) = X(2)'Old + 2
15assign_all_arr_incr.adb:2: (post)- assign_all_arr_incr:X(3) = X(3)'Old + 3
16assign_all_arr_incr.adb:2: (post)- assign_all_arr_incr:X(4) = X(4)'Old + 4
17assign_all_arr_incr.adb:2: (post)- assign_all_arr_incr:X(5) = X(5)'Old + 5
18assign_all_arr_incr.adb:2: (post)- assign_all_arr_incr:X(6) = X(6)'Old + 6
19assign_all_arr_incr.adb:2: (post)- assign_all_arr_incr:X(7) = X(7)'Old + 7
20assign_all_arr_incr.adb:2: (post)- assign_all_arr_incr:X(8) = X(8)'Old + 8
21assign_all_arr_incr.adb:2: (post)- assign_all_arr_incr:X(9) = X(9)'Old + 9

Again, the preconditions on lines 1 to 10 ensure that all additions on line 5 of Assign_All_Arr_Incr will not overflow. These preconditions are really the most precise ones for subprogram Assign_All_Arr_Incr, guaranteeing that no run-time error can be raised during subprogram execution.

Assign_All_Arr_Incr_Unk

Take yet another variation over the previous example that uses arrays of unbounded size:

1with Assign_Arr_Unk; use Assign_Arr_Unk;
2procedure Assign_All_Arr_Incr_Unk (X : in out Arr) is
3begin
4   for J in X'Range loop
5      X (J) := X (J) + J;
6   end loop;
7end Assign_All_Arr_Incr_Unk;

On this subprogram, Inspector generates the following contract:

1assign_all_arr_incr_unk.adb:2: (pre)- assign_all_arr_incr_unk:(overflow check) X'Length = 0 or X(J)'Initialized
2assign_all_arr_incr_unk.adb:2: (pre)- assign_all_arr_incr_unk:(overflow check) X'Length = 0 or X(X'First)'Initialized
3assign_all_arr_incr_unk.adb:2: (pre)- assign_all_arr_incr_unk:(overflow check) X'Length = 0 or X(X'Last)'Initialized
4assign_all_arr_incr_unk.adb:2: (post)- assign_all_arr_incr_unk:X(..) = One-of{X(J)'Old, (for _I in X'Range => _I + X(_I))}

Here, Inspector generates no precondition to guarantee that the addition on line 5 of Assign_All_Arr_Incr_Unk cannot overflow. However, Inspector does not issue any error on this subprogram. This is because these errors are suppressed by default, because they are likely false alarms.

Bad_Assign_All_Arr_Unk

Now take the subprogram just seen, and use an unbounded array instead:

1with Assign_Arr_Unk; use Assign_Arr_Unk;
2procedure Bad_Assign_All_Arr_Unk (X : in out Arr) is
3begin
4   for J in X'Range loop
5      X (J) := X (J + 1);
6   end loop;
7end Bad_Assign_All_Arr_Unk;

Inspector can still detect that the last iteration accesses the array at index X'Last + 1, because it treats array bounds symbolically:

1bad_assign_all_arr_unk.adb:5:16: high: array index check [CWE 120] (Inspector): check fails here; requires X'First <= J + 1 and J + 1 <= X'Last

14.2.2.4. Array Treatments

Let's consider now more general array treatments, in which subprograms iterate over arrays to update their content.

Map

Take the subprogram Map which stores in array Z an incremented version of array X:

1with Assign_Arr; use Assign_Arr;
2procedure Map (X : in Arr; Y : in Integer; Z : out Arr) is
3begin
4   for J in X'Range loop
5      Z (J) := X (J) + Y;
6   end loop;
7end Map;

On this subprogram, Inspector generates the following contract:

 1map.adb:2: (pre)- map:(validity check) X(1)'Initialized
 2map.adb:2: (pre)- map:(validity check) X(10)'Initialized
 3map.adb:2: (pre)- map:(validity check) X(2)'Initialized
 4map.adb:2: (pre)- map:(validity check) X(3)'Initialized
 5map.adb:2: (pre)- map:(validity check) X(4)'Initialized
 6map.adb:2: (pre)- map:(validity check) X(5)'Initialized
 7map.adb:2: (pre)- map:(validity check) X(6)'Initialized
 8map.adb:2: (pre)- map:(validity check) X(7)'Initialized
 9map.adb:2: (pre)- map:(validity check) X(8)'Initialized
10map.adb:2: (pre)- map:(validity check) X(9)'Initialized
11map.adb:2: (pre)- map:(overflow check) Y + X(1) in -2_147_483_648..2_147_483_647
12map.adb:2: (pre)- map:(overflow check) Y + X(10) in -2_147_483_648..2_147_483_647
13map.adb:2: (pre)- map:(overflow check) Y + X(2) in -2_147_483_648..2_147_483_647
14map.adb:2: (pre)- map:(overflow check) Y + X(3) in -2_147_483_648..2_147_483_647
15map.adb:2: (pre)- map:(overflow check) Y + X(4) in -2_147_483_648..2_147_483_647
16map.adb:2: (pre)- map:(overflow check) Y + X(5) in -2_147_483_648..2_147_483_647
17map.adb:2: (pre)- map:(overflow check) Y + X(6) in -2_147_483_648..2_147_483_647
18map.adb:2: (pre)- map:(overflow check) Y + X(7) in -2_147_483_648..2_147_483_647
19map.adb:2: (pre)- map:(overflow check) Y + X(8) in -2_147_483_648..2_147_483_647
20map.adb:2: (pre)- map:(overflow check) Y + X(9) in -2_147_483_648..2_147_483_647
21map.adb:2: (post)- map:Z(1) = Y + X(1)
22map.adb:2: (post)- map:Z(1..10)'Initialized
23map.adb:2: (post)- map:Z(10) = Y + X(10)
24map.adb:2: (post)- map:Z(2) = Y + X(2)
25map.adb:2: (post)- map:Z(3) = Y + X(3)
26map.adb:2: (post)- map:Z(4) = Y + X(4)
27map.adb:2: (post)- map:Z(5) = Y + X(5)
28map.adb:2: (post)- map:Z(6) = Y + X(6)
29map.adb:2: (post)- map:Z(7) = Y + X(7)
30map.adb:2: (post)- map:Z(8) = Y + X(8)
31map.adb:2: (post)- map:Z(9) = Y + X(9)

The overflow check preconditions are sufficient to guarantee that the addition on line 5 of Map cannot overflow, so no messages are generated.

Concat

Consider the program Concat which takes two arrays of unbounded size in input, and returns their concatenation in array parameter Z:

 1with Assign_Arr_Unk; use Assign_Arr_Unk;
 2procedure Concat (X, Y : in Arr; Z : out Arr) is
 3   K : Integer := 1;
 4begin
 5   for J in X'Range loop
 6      Z (K) := X (J);
 7      K := K + 1;
 8   end loop;
 9   for J in Y'Range loop
10      Z (K) := Y (J);
11      K := K + 1;
12   end loop;
13end Concat;

On this subprogram, Inspector generates the following contract:

 1concat.adb:2: (pre)- concat:(overflow check) X'Length <= 2_147_483_646
 2concat.adb:2: (pre)- concat:(array index check) X'Length = 0 or X'Last - X'First < Z'Last
 3concat.adb:2: (pre)- concat:X'Length = 0 or X(J)'Initialized
 4concat.adb:2: (pre)- concat:(array index check) X'Length = 0 or Z'First <= 1
 5concat.adb:2: (pre)- concat:(array index check) X'Length = 0 or Z'First <= X'Last - X'First + 1
 6concat.adb:2: (pre)- concat:(array index check) X'Length = 0 or Z'Last >= 1
 7concat.adb:2: (pre)- concat:(aliasing check) Y'Address /= Z'Address
 8concat.adb:2: (pre)- concat:(overflow check) Y'Length <= 2_147_483_646
 9concat.adb:2: (pre)- concat:(overflow check) Y'Length = 0 or Y(J)'Initialized
10concat.adb:2: (pre)- concat:(array index check) Z'Last <= X'Last - X'First + 6_442_450_942
11concat.adb:2: (post)- concat:Z(..) = One-of{Z(..)'Old, (for _I in Z'Range => X((X'First + _I) - 1)), (for _I in Z'Range => Y(Y'First + (_I - K)))}

The precondition on line 4 guarantees that the assignment to Z(K) on line 6 of Concat is within bounds: either X is empty (then the loop is not executed), or it should fit in Z.

This time, when run in a deep mode, Inspector issues errors for possible index checks and overflow checks failure:

1concat.adb:10:7: medium: array index check [CWE 120] (Inspector): requires K <= Z'Last
2concat.adb:10:7: medium: array index check [CWE 120] (Inspector): requires Z'First <= K
3concat.adb:11:14: low: overflow check [CWE 190] (Inspector): requires K /= Integer_32'Last

Concat_Op

Of course, the behavior of Concat can be obtained by simply using the Ada concatenation operator, as in subprogram Concat_Op:

1with Assign_Arr_Unk; use Assign_Arr_Unk;
2procedure Concat_Op (X, Y : in Arr; Z : out Arr) is
3begin
4   Z := X & Y;
5end Concat_Op;

On this subprogram, Inspector generates the following contract:

1concat_op.adb:2: (pre)- concat_op:X'First /= 2_147_483_647
2concat_op.adb:2: (pre)- concat_op:(overflow check) X'Length + Y'Length in 2..4_294_967_296
3concat_op.adb:2: (pre)- concat_op:X'Length in 1..4_294_967_295
4concat_op.adb:2: (pre)- concat_op:Y'Length in 1..4_294_967_295
5concat_op.adb:2: (post)- concat_op:possibly_updated(Z(..))

The preconditions above are not sufficient to guarantee that the bounds of Z allow such an assignment, hence the error message generated by Inspector in a deep mode:

1concat_op.adb:4:11: medium: array index check [CWE 120] (Inspector): requires Z'Length = ((if X'Length + Y'Length = 0 then Y'First else if X'Length /= 0 then X'First else Y'First)..(if X'Length + Y'Length = 0 then Y'Last else if X'Length /= 0 then X'First else Y'First + X'Length + Y'Length - 1))'Length

14.2.2.5. Limitations

Rec_Constant

Although recursion and loops are in theory equivalent, Inspector treats them very differently. While the analysis of loops is carefully designed to recognize invariants over induction variables, the analysis of recursive functions may be less precise. Take as a contrived example a simple recursive function returning a constant value:

1function Rec_Constant (X : in Integer) return Integer is
2begin
3   if X < 10 then
4      return 3;
5   else
6      return Rec_Constant (5);
7   end if;
8end Rec_Constant;

On this subprogram, Inspector generates the following contract:

1rec_constant.adb:1: (post)- rec_constant:rec_constant'Result'Initialized

The most precise postcondition would state that Rec_Constant always return the value 3.

Ident_Arr

Inspector can follow quite precisely the value of individual elements in an array, as well as the possible values for all other elements which are not individually tracked. This is not the same as being able to handle arbitrary quantified expressions over arrays, although in the cases of loop with static bounds, Inspector will automatically unroll these loops, leading to precise messages when the loop is not too large. Take for example a subprogram Ident_Arr that initializes an array to the identity function over its indexes:

1procedure Ident_Arr (X : out Arr) is
2begin
3   for J in X'Range loop
4      X (J) := J;
5   end loop;
6end Ident_Arr;

Using the aspect syntax of Ada 2012, the user can even specify a postcondition for this procedure stating the property above:

1with Assign_Arr; use Assign_Arr;
2procedure Ident_Arr (X : out Arr) with
3  Post => (for all J in X'Range => X (J) = J);

which is proven by Inspector.

Sum_All_Arr

Inspector does not treat specially some quite useful abstractions over array content, like the sum/max/min over array elements, but again, as long as the loop has static bounds and is not too large, Inspector is able to unroll such loops and produce the most precise analysis. Consider a subprogram Sum_All_Arr which computes the sum over the elements of an array:

1with Assign_Arr; use Assign_Arr;
2function Sum_All_Arr (X : in Arr) return Integer is
3   Sum : Integer := 0;
4begin
5   for J in X'Range loop
6      Sum := Sum + X (J);
7   end loop;
8   return Sum;
9end Sum_All_Arr;

Inspector will generate preconditions stating that this sum cannot overflow:

 1sum_all_arr.adb:2: (pre)- sum_all_arr:(overflow check) X(1) + X(2) + X(3) + X(4) + X(5) + X(6) + X(7) + X(8) + X(9) + X(10) in -2_147_483_648..2_147_483_647
 2sum_all_arr.adb:2: (pre)- sum_all_arr:(overflow check) X(1) + X(2) + X(3) + X(4) + X(5) + X(6) + X(7) + X(8) + X(9) in -2_147_483_648..2_147_483_647
 3sum_all_arr.adb:2: (pre)- sum_all_arr:(overflow check) X(1) + X(2) + X(3) + X(4) + X(5) + X(6) + X(7) + X(8) in -2_147_483_648..2_147_483_647
 4sum_all_arr.adb:2: (pre)- sum_all_arr:(overflow check) X(1) + X(2) + X(3) + X(4) + X(5) + X(6) + X(7) in -2_147_483_648..2_147_483_647
 5sum_all_arr.adb:2: (pre)- sum_all_arr:(overflow check) X(1) + X(2) + X(3) + X(4) + X(5) + X(6) in -2_147_483_648..2_147_483_647
 6sum_all_arr.adb:2: (pre)- sum_all_arr:(overflow check) X(1) + X(2) + X(3) + X(4) + X(5) in -2_147_483_648..2_147_483_647
 7sum_all_arr.adb:2: (pre)- sum_all_arr:(overflow check) X(1) + X(2) + X(3) + X(4) in -2_147_483_648..2_147_483_647
 8sum_all_arr.adb:2: (pre)- sum_all_arr:(overflow check) X(1) + X(2) + X(3) in -2_147_483_648..2_147_483_647
 9sum_all_arr.adb:2: (pre)- sum_all_arr:(overflow check) X(1) + X(2) in -2_147_483_648..2_147_483_647
10sum_all_arr.adb:2: (pre)- sum_all_arr:(validity check) X(1)'Initialized
11sum_all_arr.adb:2: (pre)- sum_all_arr:(validity check) X(10)'Initialized
12sum_all_arr.adb:2: (pre)- sum_all_arr:(validity check) X(2)'Initialized
13sum_all_arr.adb:2: (pre)- sum_all_arr:(validity check) X(3)'Initialized
14sum_all_arr.adb:2: (pre)- sum_all_arr:(validity check) X(4)'Initialized
15sum_all_arr.adb:2: (pre)- sum_all_arr:(validity check) X(5)'Initialized
16sum_all_arr.adb:2: (pre)- sum_all_arr:(validity check) X(6)'Initialized
17sum_all_arr.adb:2: (pre)- sum_all_arr:(validity check) X(7)'Initialized
18sum_all_arr.adb:2: (pre)- sum_all_arr:(validity check) X(8)'Initialized
19sum_all_arr.adb:2: (pre)- sum_all_arr:(validity check) X(9)'Initialized
20sum_all_arr.adb:2: (post)- sum_all_arr:sum_all_arr'Result = X(1) + X(2) + X(3) + X(4) + X(5) + X(6) + X(7) + X(8) + X(9) + X(10)
21sum_all_arr.adb:2: (post)- sum_all_arr:sum_all_arr'Result'Initialized

14.2.3. Pointer Examples

This section presents the results of running GNAT SAS' Inspector on subprograms containing pointers.

14.2.3.1. Dereference

Inspector assumes that the strong typing guarantees provided by Ada are not circumvented by the improper use of unchecked conversions and unchecked deallocation. In particular, it assumes that there are no dangling pointers. Thus, it is equivalent to state that pointer X can be dereferenced and that X is not null.

Deref

Consider an access type Ptr on integers, and a function Deref.Read which takes a parameter of type Ptr:

1package Deref is
2   type Ptr is access all Integer;
3   function Read (X : in Ptr) return Integer;
4end Deref;

It simply returns the value pointed to:

1package body Deref is
2   function Read (X : in Ptr) return Integer is
3   begin
4      return X.all;
5   end Read;
6end Deref;

On this subprogram, Inspector generates the following contract:

1deref.adb:2: (pre)- deref.read:(access check) X /= null
2deref.adb:2: (pre)- deref.read:(validity check) X.all'Initialized
3deref.adb:2: (post)- deref.read:deref.read'Result = X.all
4deref.adb:2: (post)- deref.read:deref.read'Result'Initialized

This is indeed the most precise contract for subprogram Deref.Read. The preconditions on lines 1 and 2 guarantee that no run-time error can be raised and that no uninitialized value can be read. The postcondition on line 3 completely summarizes the effect of calling Deref.Read.

Bad_Deref

Consider a version of Deref in which parameter X is an in out parameter that is always reset to null:

1with Deref; use Deref;
2function Bad_Deref (X : in out Ptr) return Integer is
3begin
4   if X /= null then
5      X := null;
6   end if;
7   return X.all;
8end Bad_Deref;

Inspector detects that X is null at the point of dereference, and it generates an error:

1bad_deref.adb:2:1: high warning: subp always fails (Inspector): bad_deref fails for all possible inputs
2bad_deref.adb:7:13: high: access check [CWE 476] (Inspector): check fails here; requires X /= null

Deref_Assign

Consider now subprogram Deref_Assign which updates the value pointed to by X:

1with Deref; use Deref;
2procedure Deref_Assign (X : in Ptr; Y : in Integer) is
3begin
4   X.all := Y;
5end Deref_Assign;

On this subprogram, Inspector generates the following contract:

1deref_assign.adb:2: (pre)- deref_assign:(access check) X /= null
2deref_assign.adb:2: (post)- deref_assign:X.all = Y
3deref_assign.adb:2: (post)- deref_assign:X.all'Initialized

Again, this is the most precise contract for subprogram Deref_Assign. The precondition on line 1 guarantees that no run-time error can be raised. The postcondition on line 2 completely summarizes the effect of calling Deref_Assign.

Bad_Deref_Assign

Consider a version of Deref_Assign in which some defensive code wrongly returns when parameter X is not null:

1with Deref; use Deref;
2procedure Bad_Deref_Assign (X : in Ptr; Y : in Integer) is
3begin
4   if X /= null then
5      return;
6   end if;
7   X.all := Y;
8end Bad_Deref_Assign;

Inspector detects that X is null at the point of dereference, and it generates an error:

1bad_deref_assign.adb:7:6: high: access check [CWE 476] (Inspector): check fails here; requires X /= null

Self_Deref_Assign

Finally, consider the version of Self_Assign seen previously, where X is now a pointer:

1with Deref; use Deref;
2procedure Self_Deref_Assign (X : in out Ptr) is
3begin
4   X.all := X.all + 1;
5end Self_Deref_Assign;

On this subprogram, Inspector generates the following contract:

1self_deref_assign.adb:2: (pre)- self_deref_assign:(access check) X /= null
2self_deref_assign.adb:2: (pre)- self_deref_assign:(overflow check) X.all /= 2_147_483_647
3self_deref_assign.adb:2: (post)- self_deref_assign:X.all /= -2_147_483_648
4self_deref_assign.adb:2: (post)- self_deref_assign:X.all = X.all'Old + 1

The preconditions on lines 1 and 2 guarantee that that no run-time error can be raised and that no uninitialized value can be read. Note that the precondition on line 2 implicitly states that X.all is initialized, since it expresses a relation involving X.all. The lines 2-4 are exactly the same as the contract previously seen for Self_Assign, where X has been replaced by X.all.

Bad_Self_Deref_Assign

Consider a version of Self_Deref_Assign in which the increment is missing:

1with Deref; use Deref;
2procedure Bad_Self_Deref_Assign (X : in out Ptr) is
3begin
4   X.all := X.all;
5end Bad_Self_Deref_Assign;

Inspector detects that the assignment is useless, and in a deep mode, it generates a warning:

1bad_self_deref_assign.adb:4:10: medium warning: useless reassignment [CWE 563] (Inspector): useless reassignment of X.all

14.2.3.2. Aliasing

Pointer_Assign

What makes pointers so tricky to handle in static analysis is the possibility of aliasing, where two pointers point to the same memory location. Consider the subprogram Pointer_Assign:

1with Deref; use Deref;
2procedure Pointer_Assign (X, Y : in Ptr) is
3begin
4   X.all := Y.all + 1;
5end Pointer_Assign;

On this subprogram, Inspector generates the following contract:

1pointer_assign.adb:2: (pre)- pointer_assign:(access check) X /= null
2pointer_assign.adb:2: (pre)- pointer_assign:(access check) Y /= null
3pointer_assign.adb:2: (pre)- pointer_assign:(overflow check) Y.all /= 2_147_483_647
4pointer_assign.adb:2: (post)- pointer_assign:X.all /= -2_147_483_648
5pointer_assign.adb:2: (post)- pointer_assign:X.all = Y.all + 1

Preconditions are the expected ones. The interesting postcondition is the one on line 5, which states the relationship between the input value of Y.all and the output value of X.all. Indeed, this is true whether X and Y are equal or not. (Note that on this example, Inspector displays Y.all for the input value pointed to by Y.)

Bad_Pointer_Assign

Consider a version of Pointer_Assign in which the user added an assertion that, after the assignment, X.all and Y.all should indeed be different:

1with Deref; use Deref;
2procedure Bad_Pointer_Assign (X, Y : in Ptr) is
3begin
4   X.all := Y.all + 1;
5   pragma Assert (X.all = Y.all + 1);
6end Bad_Pointer_Assign;

This is not true in the case where X and Y are equal. In a deep mode, Inspector detects this possibility, and it generates an error:

1bad_pointer_assign.adb:5:19: low: assertion (Inspector): requires Y.all = Y.all
2bad_pointer_assign.adb:5:33: low: overflow check [CWE 190] (Inspector): requires Y.all /= Integer_32'Last

The message indicates that the value of Y.all at subprogram entry, and the value of Y.all after the assignment, may differ. This is the case precisely when X and Y are equal.

Call_Pointer_Assign

Let's see how Inspector handles a caller of Pointer_Assign which aliases its parameters X and Y:

1with Deref; use Deref;
2with Pointer_Assign;
3procedure Call_Pointer_Assign (X : in Ptr) is
4begin
5   Pointer_Assign (X, X);
6end Call_Pointer_Assign;

On this subprogram, when run in a deep mode, Inspector generates the following contract:

1call_pointer_assign.adb:3: (pre)- call_pointer_assign:(access check) X /= null
2call_pointer_assign.adb:3: (pre)- call_pointer_assign:(overflow check) X.all /= 2_147_483_647
3call_pointer_assign.adb:3: (post)- call_pointer_assign:X.all /= -2_147_483_648
4call_pointer_assign.adb:3: (post)- call_pointer_assign:X.all = X.all'Old + 1

The postcondition of Pointer_Assign has lead to a similar postcondition for Call_Pointer_Assign, where Y.all has correctly been replaced with X.all'Old (and not X.all).

Double_Pointer_Assign

Consider now the subprogram Double_Pointer_Assign which assigns to both X and Y pointers:

1with Deref; use Deref;
2procedure Double_Pointer_Assign (X, Y : in Ptr) is
3begin
4   X.all := 1;
5   Y.all := 2;
6end Double_Pointer_Assign;

On this subprogram, Inspector generates the following contract:

1double_pointer_assign.adb:2: (pre)- double_pointer_assign:(access check) X /= null
2double_pointer_assign.adb:2: (pre)- double_pointer_assign:(access check) Y /= null
3double_pointer_assign.adb:2: (post)- double_pointer_assign:X.all in 1..2
4double_pointer_assign.adb:2: (post)- double_pointer_assign:Y.all = 2

Note the asymmetry between postconditions 3 and 4 which deal respectively with the output values of X.all and Y.all. Since Y.all is assigned last, its output value 2 comes from this last assignment. On the contrary, the output value of X.all can come either from the explicit assignment to X.all, or from the assignment to Y.all if X and Y are equal. Therefore, the postcondition on line 3 correctly considers the two output values for X.all.

The postcondition on line 3 is not the most precise postcondition, which would state in which case the value of X.all is 1 or 2:

(X /= Y and X.all = 1) or (X = Y and X.all = 2)

As already seen in section Basic Examples in the context of branchings, Inspector generates internally this more precise postcondition, from which it outputs on line 3 a more user-friendly postcondition. The internal more precise postcondition is used for analyzing callers of Double_Pointer_Assign.

Call_Double_Pointer_Assign

Take now such a caller Call_Double_Pointer_Assign, which calls Double_Pointer_Assign on non-aliased parameters:

1package Call_Double_Pointer_Assign is
2   X, Y : aliased Integer;   
3   procedure Call;
4end Call_Double_Pointer_Assign;
1with Deref; use Deref;
2with Double_Pointer_Assign;
3package body Call_Double_Pointer_Assign is
4   procedure Call is
5   begin
6      Double_Pointer_Assign (X'Access, Y'Access);
7   end Call;
8end Call_Double_Pointer_Assign;

On this subprogram, Inspector generates the following contract:

1call_double_pointer_assign.adb:4: (post)- call_double_pointer_assign.call:X = 1
2call_double_pointer_assign.adb:4: (post)- call_double_pointer_assign.call:Y = 2

As mentioned before, Inspector could use the more precise contract for Double_Pointer_Assign, which leads here to the precise postcondition that X has value 1 after the call.

14.2.4. Unknown Subprograms

This section presents the results of running GNAT SAS' Inspector on partial applications. It is very common to analyze a partial application, where some units are not yet implemented. This may happen either when analyzing a library, or when not having access to parts of an application (third-party development) or when analyzing an application early on, before all the code has been developed. Inspector handles these cases gracefully, by assuming that calling an unknown subprogram has some effects on its calling environment. These assumptions are precisely tuned to avoid false alarms, while keeping the analysis as precise as possible.

Call_Unknown

Take the unknown subprogram Unknown:

1procedure Unknown (X : out Integer; Y : in Integer);

It is called by subprogram Call_Unknown:

1with Unknown;
2procedure Call_Unknown (X : out Integer) is
3begin
4   Unknown (X, 1);
5end Call_Unknown;

On this subprogram, Inspector generates the following contract:

1call_unknown.adb:2: (post)- call_unknown:X'Initialized

Because the parameter X of Unknown is marked as out, Inspector assumes that it is initialized by the call to Unknown, hence the corresponding postcondition of Call_Unknown.

Call_Unknown_Pos

Now consider a slight variation of Call_Unknown in which X is a positive integer:

1with Unknown;
2procedure Call_Unknown_Pos (X : out Positive) is
3begin
4   Unknown (X, 1);
5end Call_Unknown_Pos;

On this subprogram, Inspector generates the following contract:

1call_unknown_pos.adb:2: (presumption)- call_unknown_pos:unknown.X@4 >= 1
2call_unknown_pos.adb:2: (post)- call_unknown_pos:X'Initialized

The postcondition on line 2 is similar to the one seen previously. What is new here is the presumption on line 1, which states that Inspector assumes that Unknown returns a positive value for X, as needed to avoid a possible run-time error in calling Unknown. Note the name X@4 is used instead of X to denote the value of X on line 4, after the call to Unknown. Since this presumption is relative to the call in Call_Unknown_Pos, it is part of the contract of Call_Unknown_Pos.

Call_Unknown_Rel

Presumptions can be more complex, for example in the subprogram Call_Unknown_Rel, an assertion requires that the value of X returned by Unknown respects a linear relation with Y:

1with Unknown;
2procedure Call_Unknown_Rel (X : out Integer; Y : in Integer) is
3begin
4   Unknown (X, Y);
5   pragma Assert (X < 2 * Y + 1);
6end Call_Unknown_Rel;

On this subprogram, Inspector generates the following contract:

1call_unknown_rel.adb:2: (pre)- call_unknown_rel:(overflow check) Y in -1_073_741_824..1_073_741_823
2call_unknown_rel.adb:2: (presumption)- call_unknown_rel:unknown.X@4 - Y*2 in -4_294_967_294..0
3call_unknown_rel.adb:2: (presumption)- call_unknown_rel:unknown.X@4 /= 2_147_483_647
4call_unknown_rel.adb:2: (post)- call_unknown_rel:X - Y*2 in -4_294_967_294..0
5call_unknown_rel.adb:2: (post)- call_unknown_rel:X /= 2_147_483_647

The presumption on line 2 indeed propagates the desired assertion on the expected behavior of Unknown.

Call_Unknown_Arr

Inspector tries as much as possible to keep precise information through calls to unknown subprograms. Take the unknown subprogram Unknown_Arr which takes an array as in out parameter:

1with Assign_Arr; use Assign_Arr;
2procedure Unknown_Arr (X : in out Arr);

It is called in a context where the value of some array elements is known:

1with Assign_Arr; use Assign_Arr;
2with Unknown_Arr;
3procedure Call_Unknown_Arr (X : in out Arr) is
4begin
5   X (1) := 1;
6   X (4) := 2;
7   Unknown_Arr (X);
8end Call_Unknown_Arr;

On this subprogram, Inspector generates the following contract:

1call_unknown_arr.adb:3: (post)- call_unknown_arr:X(1 | 4) = X(1 | 4)@7

Since the array is passed as in out, Inspector assumes that the value of X may change, but it keeps the information that elements at indexes 1 and 4 are initialized.

Call_Unknown_Ptr

This is even more visible in the way Inspector analyzes calls to unknown subprograms which take pointers in parameter, like Unknown_Ptr:

1with Deref; use Deref;
2procedure Unknown_Ptr (X : in out Ptr);

It is called twice, first in a context where the value of parameter X.all is not known, then in a context where it is known:

1with Deref; use Deref;
2with Unknown_Ptr;
3procedure Call_Unknown_Ptr (X, Y : in out Ptr) is
4begin
5   Unknown_Ptr (X);
6   Y.all := 1;
7   Unknown_Ptr (Y);
8   pragma Assert (X.all < Y.all);
9end Call_Unknown_Ptr;

On this subprogram, Inspector generates the following contract:

1call_unknown_ptr.adb:3: (pre)- call_unknown_ptr:(access check) Y /= null
2call_unknown_ptr.adb:3: (presumption)- call_unknown_ptr:unknown_ptr.X@5 /= null
3call_unknown_ptr.adb:3: (presumption)- call_unknown_ptr:unknown_ptr.X@5.all /= 2_147_483_647
4call_unknown_ptr.adb:3: (presumption)- call_unknown_ptr:unknown_ptr.X@5.all < unknown_ptr.X@7.all
5call_unknown_ptr.adb:3: (presumption)- call_unknown_ptr:unknown_ptr.X@7 /= null
6call_unknown_ptr.adb:3: (presumption)- call_unknown_ptr:unknown_ptr.X@7.all /= -2_147_483_648
7call_unknown_ptr.adb:3: (post)- call_unknown_ptr:X /= null
8call_unknown_ptr.adb:3: (post)- call_unknown_ptr:Y /= null
9call_unknown_ptr.adb:3: (post)- call_unknown_ptr:Y.all = 1

The presumption on line 4 shows that Inspector assumes that the first call to Unknown_Ptr (on line 5 of Call_Unknown_Ptr) returns a value for X.all that is less than the value of Y.all returned by the second call to Unknown_Ptr (on line 7 of Call_Unknown_Ptr) as specified by the following assertion.

Note that the postconditions on line 7 and 8 are derived from presumptions.

Above_Call_Unknown

In general, the exact constraint expected from a call to an unknown subprogram may not be given by its direct caller, but possibly by a caller higher in the call chain. Inspector handles these cases by generating presumptions on unknown subprograms in the contract of these indirect callers. Consider a caller of the subprogram Call_Unknown, which expects X to be different from 10 after the call:

1with Call_Unknown;
2procedure Above_Call_Unknown (X : out Integer) is
3begin
4   Call_Unknown (X);
5   pragma Assert (X /= 10);
6end Above_Call_Unknown;

On this subprogram, Inspector generates the following contract:

1above_call_unknown.adb:2: (presumption)- above_call_unknown:unknown.X@4 /= 10
2above_call_unknown.adb:2: (post)- above_call_unknown:X /= 10

The presumption on line 1 indeed requires that the call to Unknown inside Call_Unknown returns a value different from 10. Since this presumption is relative to the call in Call_Unknown inside a call in Above_Call_Unknown, it is part of the contract of Above_Call_Unknown.

14.3. Infer's Limitations and Heuristics

Since Infer is a static analysis framework, it can use different analyses to emit different messages, and each analysis can have distinct limitations. The following table summarizes these limitations:

Message kinds

Limitations and heuristics

access check, memory leak, use after free, function with side effects, function with side effects in conditional

The checker typically doesn't follow all paths through the subprogram and can thus miss errors.

unitialized value passed to

The checker is intraprocedural and field-insensitive. It only checks whether any field of a composite type object is written before the object is passed as an IN or IN-OUT parameter to a call. Consequently, it misses issues when the written field is not the one that is later read.

The checker doesn't emit messages on tagged types primitives and null records.

write without open, read without open, double open, close without open

The checker is intraprocedural, meaning messages are emitted only for problems in a single subprogram.