2

I was reading up on array types in Ada and found it interesting that, unlike C++, the language allows their size to be unknown at compile time. I was not sure how they are implemented though so I wrote a small test:

with Ada.Text_IO; use Ada.Text_IO;
with Ada.Command_Line; use Ada.Command_Line;

procedure Main is
   type Data is array (1 .. 131_072) of Integer;
   type Vector is array (Positive range <>) of Data;
   Sequence : Vector (1 .. Argument_Count);
begin
   for I in Sequence'Range loop
      Sequence (I) := (others => I);
      Put (Integer'Image (Sequence (I)(1)));
   end loop;
end Main;

And then tried to replicate this code in C using variable length arrays:

#include <stdio.h>

struct data {
    int x[131072];
};

int main(int argc, char** argv) {
    (void)argv;
    struct data sequence[argc - 1];
    for (int i = 0; i < argc - 1; i++) {
        for (int j = 0; j < 131072; j++)
            sequence[i].x[j] = i;
        printf("%i ", sequence[i].x[1]);
    }
}

I compiled both with gnatmake -gnato -fstack-check -gnat2012 -gnata -O3 main.adb -o main and gcc -O3 -Wall -Werror -Wextra -pedantic cmain.c -o cmain. After running the programs, both were failing when given 16 or more arguments - the difference was that cmain simply segfaulted while main ended up raising "STORAGE_ERROR : stack overflow or erroneous memory access".

Since both VLAs and unconstrained arrays seem to be (at least on surface) implemented in a similar manner and the former is widely considered to be not safe to use in nearly all circumstances, is it safe to use the latter?

GDI512
  • 183
  • 1
  • 9
  • 1
    Why are VLAs unsafe? It's just that stack size is typically more limited than heap size but if you make the x array large enough you'll run into the exact same problems with a malloc. – Peter - Reinstate Monica Mar 31 '21 at 19:20
  • 1
    Apart from that questionable premise: Failing with a constraint error is what makes Ada safe. The segfault in the C program may occur only after all kinds of damage has been done to your data, undetected. – Peter - Reinstate Monica Mar 31 '21 at 19:23
  • Thanks, I guess that's the answer I was looking for there. But just to be clear my assumptions about VLA are based on https://stackoverflow.com/questions/1018853/why-is-the-use-of-alloca-not-considered-good-practice and https://stackoverflow.com/questions/7326547/is-it-safe-to-use-variable-length-arrays/7326654#7326654. – GDI512 Mar 31 '21 at 19:32
  • @Peter-ReinstateMonica creating too large VLA is UB, while `malloc` would return `NULL` in such case. – Ruslan Mar 31 '21 at 20:01
  • Yes they are safe to use. Worst that can happen is a "storage error" exception when they exceed the available space (that may be 10s or 100s of MB unless you're on an embedded system). –  Apr 01 '21 at 12:58

2 Answers2

4

Unconstrained array types in Ada are not by themselves unsafe; what is unsafe, or at least may lead to an exception, is using an unchecked input value (such as Argument_Count) to create an array object of that size. If you do that, and don't have an exception handler, an attacker can make your program abort with an unhandled exception, as in your example.

Note that unconstrained array types are used in Ada in two ways:

  1. To create array objects of a dynamically determined size, as in your example.
  2. To pass arrays of various sizes as parameters to subprograms, even if the actual array objects have statically defined sizes.

The second use (parameters) is completely safe, of course, and the actual bounds of the parameter can be accessed as usual by A'First, A'Last, A'Range, A'Length.

Niklas Holsti
  • 1,907
  • 3
  • 9
  • Is it possible that the program will be aborted anyway because there will not be enough stack space left for the exception handler? – GDI512 Apr 01 '21 at 23:54
  • 1
    That should not happen. An Ada exception handler always covers the sequence of statements (not declarations) preceding the exception handler. For example, in a procedure: `procedure Foo (...) is begin exception end Foo` , the exception handle exceptions that happen in the , but not exceptions, such as stack overflow, that happen in the . Exceptions in are propagated to the caller of the procedure to be handled by the there (if any), and we know the caller has enough stack space. – Niklas Holsti Apr 03 '21 at 07:34
  • 1
    (Continuing.) This means that exceptions in the declarations of the main procedure cannot be handled by the handlers of the main procedure. However, there is an easy fix: make the statements of the main procedure into a "block statement", `declare begin end`, and then the main procedure's exception handlers can handle exceptions in the of the block statement, because the block statement is a statement, not a declaration. – Niklas Holsti Apr 03 '21 at 07:56
2

Unconstrained arrays are not intrinsically unsafe. See the example below implementing parallel addition of array elements.

package Parallel_Addition is
   type Data_Array is array(Integer range <>) of Integer;
   type Data_Access is access all Data_Array;
   function Sum(Item : in not null Data_Access) return Integer;
end Parallel_Addition;

Note that the package specification above declares an unconstrained array type. It also declares an access type to the unconstrained array type so that very large arrays can be dynamically allocated, avoiding stack exhaustion problems.

package body Parallel_Addition is

   ---------
   -- Sum --
   ---------

   function Sum (Item : in not null Data_Access) return Integer is
      task type Adder is
         entry Set (Min : Integer; Max : Integer);
         entry Report (Value : out Integer);
      end Adder;

      task body Adder is
         Total : Integer := 0;
         First : Integer;
         Last  : Integer;
      begin
         accept Set (Min : Integer; Max : Integer) do
            First := Min;
            Last  := Max;
         end Set;
         for I in First .. Last loop
            Total := Total + Item (I);
         end loop;
         accept Report (Value : out Integer) do
            Value := Total;
         end Report;
      end Adder;
      A1  : Adder;
      A2  : Adder;
      R1  : Integer;
      R2  : Integer;
      Mid : constant Integer := (Item'Length / 2) + Item'First;
   begin
      A1.Set (Min => Item'First, Max => Mid);
      A2.Set (Min => Mid + 1, Max => Item'Last);
      A1.Report (R1);
      A2.Report (R2);
      return R1 + R2;
   end Sum;

end Parallel_Addition;

No problems using the unconstrained array instance in the function Sum. Let's try test this package using a very large dynamically allocated array.

with Parallel_Addition; use Parallel_Addition;
with Ada.Text_IO;       use Ada.Text_IO;
with Ada.Calendar;      use Ada.Calendar;

procedure Parallel_Addition_Test is
   The_Data : Data_Access := new Data_Array (1 .. Integer'Last);
   Start    : Time;
   Stop     : Time;
   The_Sum  : Integer;

begin
   The_Data.all := (others => 1);
   Start        := Clock;
   The_Sum      := Sum (The_Data);
   Stop         := Clock;
   Put_Line ("The sum is: " & Integer'Image (The_Sum));
   Put_Line
     ("Addition elapsed time is " &
      Duration'Image (Stop - Start) &
        " seconds.");
   Put_Line
     ("Time per addition operation is " &
        Float'Image(Float(Stop - Start) / Float(The_Data'Length)) &
        " seconds.");
end Parallel_Addition_Test;

When executed on my Windows 10 PC I get the following output:

The sum is:  2147483647
Addition elapsed time is  5.141288000 seconds.
Time per addition operation is  2.39410E-09 seconds.
Jim Rogers
  • 4,822
  • 1
  • 11
  • 24