18.5 Control Flow Redundancy

GNAT can guard against unexpected execution flows, such as branching into the middle of subprograms, as in Return Oriented Programming exploits.

In units compiled with -fharden-control-flow-redundancy, subprograms are instrumented so that, every time they are called, basic blocks take note as control flows through them, and, before returning, subprograms verify that the taken notes are consistent with the control-flow graph.

The performance impact of verification on leaf subprograms can be much higher, while the averted risks are much lower on them. Instrumentation can be disabled for leaf subprograms with -fhardcfr-skip-leaf.

Functions with too many basic blocks, or with multiple return points, call a run-time function to perform the verification. Other functions perform the verification inline before returning.

Optimizing the inlined verification can be quite time consuming, so the default upper limit for the inline mode is set at 16 blocks. Command-line option --param hardcfr-max-inline-blocks= can override it.

Even though typically sparse control-flow graphs exhibit run-time verification time nearly proportional to the block count of a subprogram, it may become very significant for generated subprograms with thousands of blocks. Command-line option --param hardcfr-max-blocks= can set an upper limit for instrumentation.

For each block that is marked as visited, the mechanism checks that at least one of its predecessors, and at least one of its successors, are also marked as visited.

Verification is performed just before a subprogram returns. The following fragment:

if X then
  Y := F (Z);
  return;
end if;

gets turned into:

type Visited_Bitmap is array (1..N) of Boolean with Pack;
Visited : aliased Visited_Bitmap := (others => False);
--  Bitmap of visited blocks.  N is the basic block count.
[...]
--  Basic block #I
Visited(I) := True;
if X then
  --  Basic block #J
  Visited(J) := True;
  Y := F (Z);
  CFR.Check (N, Visited'Access, CFG'Access);
  --  CFR is a hypothetical package whose Check procedure calls
  --  libgcc's __hardcfr_check, that traps if the Visited bitmap
  --  does not hold a valid path in CFG, the run-time
  --  representation of the control flow graph in the enclosing
  --  subprogram.
  return;
end if;
--  Basic block #K
Visited(K) := True;

Verification would also be performed before tail calls, if any front-ends marked them as mandatory or desirable, but none do. Regular calls are optimized into tail calls too late for this transformation to act on it.

In order to avoid adding verification after potential tail calls, which would prevent tail-call optimization, we recognize returning calls, i.e., calls whose result, if any, is returned by the calling subprogram to its caller immediately after the call returns. Verification is performed before such calls, whether or not they are ultimately optimized to tail calls. This behavior is enabled by default whenever sibcall optimization is enabled (see -foptimize-sibling-calls); it may be disabled with -fno-hardcfr-check-returning-calls, or enabled with -fhardcfr-check-returning-calls, regardless of the optimization, but the lack of other optimizations may prevent calls from being recognized as returning calls:

--  CFR.Check here, with -fhardcfr-check-returning-calls.
P (X);
--  CFR.Check here, with -fno-hardcfr-check-returning-calls.
return;

or:

--  CFR.Check here, with -fhardcfr-check-returning-calls.
R := F (X);
--  CFR.Check here, with -fno-hardcfr-check-returning-calls.
return R;

Any subprogram from which an exception may escape, i.e., that may raise or propagate an exception that isn’t handled internally, is conceptually enclosed by a cleanup handler that performs verification, unless this is disabled with -fno-hardcfr-check-exceptions. With this feature enabled, a subprogram body containing:

--  ...
  Y := F (X);  -- May raise exceptions.
--  ...
  raise E;  -- Not handled internally.
--  ...

gets modified as follows:

begin
  --  ...
    Y := F (X);  -- May raise exceptions.
  --  ...
    raise E;  -- Not handled internally.
  --  ...
exception
  when others =>
    CFR.Check (N, Visited'Access, CFG'Access);
    raise;
end;

Verification may also be performed before No_Return calls, whether all of them, with -fhardcfr-check-noreturn-calls=always; all but internal subprograms involved in exception-raising or -reraising or subprograms explicitly marked with both No_Return and Machine_Attribute expected_throw pragmas, with -fhardcfr-check-noreturn-calls=no-xthrow (default); only nothrow ones, with -fhardcfr-check-noreturn-calls=nothrow; or none, with -fhardcfr-check-noreturn-calls=never.

When a No_Return call returns control to its caller through an exception, verification may have already been performed before the call, if -fhardcfr-check-noreturn-calls=always or -fhardcfr-check-noreturn-calls=no-xthrow is in effect. The compiler arranges for already-checked No_Return calls without a preexisting handler to bypass the implicitly-added cleanup handler and thus the redundant check, but a local exception or cleanup handler, if present, will modify the set of visited blocks, and checking will take place again when the caller reaches the next verification point, whether it is a return or reraise statement after the exception is otherwise handled, or even another No_Return call.

The instrumentation for hardening with control flow redundancy can be observed in dump files generated by the command-line option -fdump-tree-hardcfr.

For more details on the control flow redundancy command-line options, see Using the GNU Compiler Collection (GCC). These options can be used with other programming languages supported by GCC.