Static memory allocation is totally fine if the thing you're building is, like, a control system for a jumbo jet. You know exactly how many engines you have, how many wings you have, how many pilots you have, etc. Your autopilot has a fixed set of parameters: heading, speed, altitude, etc. You know exactly what tasks you're running: watch engine performance, watch the altimeter, watch cabin pressure, find the instrument landing system and line up with it, etc. If you have some new instrument to plug in, you get a new version of the flight control software, and you're certainly not plugging in new things mid-flight. Even if you aren't running all the tasks at once - e.g., you're usually not landing the plane - you want to guarantee you could turn on any of those tasks. You never want the possibility of running out of memory when you try to land, so you want to allocate memory for landing the plane at design time that's always reserved for landing the plane, and nobody would call that a "memory leak."
A general-purpose OS like Linux is different. You can create as many processes as you want. You can mount as many filesystems as you want, of various formats, whenever you want. You can plug in devices while the system is running. You can add drivers to a running kernel and remove them too. You can configure network connections, turn swap on and off, etc. Inbound network connections can show up at any time. So it needs dynamic allocation.
Ada is fantastic for problems like a flight computer. But it's a different sort of problem. For a general-purpose OS, you are allocating and freeing things in response to user input, and you want to make sure you neither leak memory nor use memory after it's been freed.
Rust is very good at solving that specific problem without the need for a GC. That's what people mean when they say "Rust doesn't use a GC."
AFAIK short of SPARK:
* Ada has pointer arithmetic, you can overflow buffers or read out of bounds.
* Dynamic allocation is straight unsafe and unchecked (unless you have a GC, in which case… you have a GC), you can use-after-free and double-free if you copy pointers beforehand.
* Accessing uninitialised variables is not forbidden.
* You can dereference null pointers (though it should cause an exception so I guess that's memory-safe).
Not true. Arrays have bounds and those are checked. However, you can do arithmetic with the type ptrdiff_t of the package Interfaces.C.Pointers. You can also do an Ada.Unchecked_Conversion on a pointer that you get from a C function. Obviously that's unsafe.
> * Dynamic allocation is straight unsafe and unchecked
If allocation on the heap fails, it will raise a Storage_Error, but you can catch it. Also, the language has a lot of restrictions on when you may copy and store a pointer.
> * Accessing uninitialised variables is not forbidden.
True, but there are pragmas like Normalize_Scalars (to initialize them to invalid values) or Initialize_Scalars.
> * You can dereference null pointers
True, but dereferencing will raise an exception. Also you can define non-null pointers like this:
type Foo_Ptr is not null access Foo;
or add a "not null" constraint to a nullable pointer type: procedure Bar (Value : not null Foo_Access);