Oh, GNAT, Why Do You Overflow Your Stack?



June 4th, 2021 by Diana Coman

Claimer, disclaimer, motto and all else: I do NOT want to write an Ada compiler.

Truth be told, I do not even really want to write (yet again!) about bugs and poor implementations and unexpected errors of all sorts. But given how easily I find them these days without even looking, they really want to be written about, to be finally brought to some light, to be seen and acknowledged at least, no matter how harshly judged or how negatively evaluated - anything, anything at all rather than the sad, endless lingering in the shadows, unseen, unmentioned, unnoticed, forever hidden among thousands and thousands of lines of code that nobody really wants to as much as look at ever again. Can you even blame the errors for wanting to be seen, perhaps even, one day, if they are very lucky indeed - to be fixed?

Deep in the 360M of GNAT's code and according to the heavy tomes of reference on the matter, there are thin Ada bindings1, thick Ada bindings2 and even supposedly Ada-native packages for input/output of all sorts3, from humble files on the disk to supposedly abstract streams. All of these can in theory handle everything and anything one might need. In practice, as it tends to happen whenever piles of code implement tomes of documentation, both the everything and the anything are rather severely and unexpectedly restricted. For instance: yes, you can supposedly read and write "any elements you define" with Direct_IO that allows even read/write from/at arbitrary positions but should you take that seriously and go about trying to write for instance as large an element as a mere 32-bits integer type allows, what you'll get is not the file's contents but an unhandled stack overflow exception when calling Direct_IO's Write method, have a look (first the code then the result of running it):

  sz: constant Integer_32 := Integer_32'Last;
  type Bytes is array(Integer_32 range <>) of Unsigned_8;
  subtype SzBytes is Bytes(1..sz);
  type ptr is access all SzBytes;

  procedure direct is
    package ndir is new Ada.Direct_IO( Element_Type => SzBytes);
    use ndir;
    n: ptr := new SzBytes;
    f: ndir.File_Type;
  begin
    Put_Line("Direct IO attempting write with element size " & sz'Image);
    n.all := (others => 10);
    Create(f, Out_File, "direct_io.txt");
    Write(f, n.all);
    Close(f);
    Put_Line("Direct IO wrote file with element size " & sz'Image);
    Delete(f);
  exception
    when others =>
      if Is_Open(f) then
        Close(f);
      end if;
  end direct;

./direct_io_fail
Word size is 64
Attempting instantiation of Direct_IO with element size 2147483647

raised STORAGE_ERROR : stack overflow or erroneous memory access

Note perhaps that the cute number there, right before the STORAGE_ERROR outrage is a perfectly valid 32-bits integer, not even going above this magic limit of 4 octets, nevermind that the poor underlying OS is 64-bits. But is it truly impossible to write with GNAT 2147483647 bytes to a file? Does any other GNAT package know how to do it silently and *without* raising exceptions? Anyone at all? Why yes, there you go, Sequential_IO running on the same machine *can* do it, wonder of wonders (and I suggest someone fixes it so that it fails too, lest it overthrows the curve and raises barriers to entry or something):

  procedure seq is
    package nseq is new Ada.Sequential_IO( Element_Type => SzBytes);
    use nseq;
    n: ptr := new SzBytes;
    f : nseq.File_Type;
  begin
    Put_Line("Sequential IO attempting write with element size " & sz'Image);
    n.all := (others => 20);
    Create(f, Out_File, "seq_io.txt");
    Write(f, n.all);
   Close(f);
    Put_Line("Sequential IO wrote file with element size " & sz'Image);
    Delete(f);
  exception
    when others =>
      if Is_Open(f) then
        Close(f);
      end if;
  end seq;

./direct_io_fail
Word size is 64
Attempting instantiation of Sequential_IO with element size 2147483647
Sequential IO attempting write with element size 2147483647
Sequential IO wrote file with element size 2147483647

Funnily enough, GNAT is sortof, kindof aware of the issue, only not quite of the full range of it, it would seem. For if one is even bolder than before and asks for an even larger element (anything above 2^33 apparently), GNAT itself points out that Direct_IO insists on not using references ever and storing instead everything locally, thus predictably overflowing the stack in short order:

gnat_fail_direct_io/io.adb:26:9: error: total size of local objects too large
gprbuild: *** compilation phase failed

Digging deeper into GNAT's own source files though yields ever more interesting comments related to this magical 2GB limit for handling files. There is first the comment related to obtaining the *size* of a file, straight from adaint.c4, aka GNAT's own wrapper of various C lib functions (in this case it's in the wrapper attempting to get various attributes of a file, via stat):

    /* st_size may be 32 bits, or 64 bits which is converted to long. We
       don't return a useful value for files larger than 2 gigabytes in
       either case. */

If the above is perhaps not interesting enough for you, don't despair, for it gets better, here's another one, this time from that thin binding over C read/write function, so head over to i-cstrea.adb (in the same dir as the previous file) and read from line 84:

   type Byte_Buffer is array (0 .. size_t'Last / 2 - 1) of Unsigned_8;
   --  This should really be 0 .. size_t'last, but there is a problem
   --  in gigi in handling such types (introduced in GCC 3 Sep 2001)
   --  since the size in bytes of this array overflows ???

At least it's all quite entertaining by now, if in a rather absurdist manner - for the layers upon layers of wrappers, the main thing you get is basically more layers and layers to unwrap in order to find the source of the error when it manifests (and then laugh at it for there isn't all that much to do about it, when it gets all the way to somewhere in the gcc, is there?). While some enjoy unwrapping even such surprises, I confess I don't quite see the point to it. So I'll skip all the wrappers with their promise of future time spent unwrapping and I'll go at least straight for the very same lib functions that are used anyway - at least I'll use them directly and thus know faster where the trouble is, if indeed there still is any.


  1. E.g. Interfaces.C.Streams in i-ctrea.ads/adb, which literally imports the usual C library functions for file handling, such as fopen, fwrite, fread, fclose. 

  2. Basically *all* of Ada's "IO" packages (e.g. Text_IO, Direct_IO, Sequential_IO) are nothing but thick bindings of the C library functions, at best, or rather at longest, since that's what it is each and every time: how many layers of packages do the calls go through in order to arrive anyway at the same basic C functions on which all rely to do the work. 

  3. Ada.Streams, supposedly an abstraction of both message and medium - in practice though they bring in so much luggage of the exact sort that I don't want to have around ever again if at all possible that I don't even have them on the list as usable at all - they got mentioned here for completeness and nothing else. 

  4. In a standard installation of Adacore's GNAT, you'll find it in /usr/gnat/lib/gcc/x86_64-pc-linux-gnu/4.9.4/rts-sjlj/adainclude/ or, if you still use zcx, then in /usr/gnat/lib/gcc/x86_64-pc-linux-gnu/4.9.4/rts-native/adainclude/ 

Comments feed: RSS 2.0

One Response to “Oh, GNAT, Why Do You Overflow Your Stack?”

  1. [...] had no choice1. A few years and a lot of experience with said GNAT implementation later, I know for a fact that the I/O packages of GNAT such as Direct_IO and Sequential_IO are broken and merely obfus... rather than helping in any significant [...]

Leave a Reply