Ada aims to restrict unsafe language features which break type safety to a set of constructs which can be easily spotted in source case. Examples include unchecked deallocation, unchecked conversion and explicit address handling. This note shows that a combination of safe-looking language features can be used to undermine type safety, too.
The standard way to show that type safety has been broken in a language is to implement a cast function Conversion
from any type Source
to any type Target
:
generic type Source is private; type Target is private; function Conversion (S : Source) return Target;
This generic function can be used like Ada.Unchecked_Conversion
:
with Ada.Text_IO; with Ada.Unchecked_Conversion; with Conversion; procedure Convert_Test is type Integer_Access is access all Integer; J : aliased Integer; J_Access : Integer_Access := J'Access; function Convert is new Conversion (Integer_Access, Integer); function Unchecked_Convert is new Ada.Unchecked_Conversion (Integer_Access, Integer); begin Ada.Text_IO.Put_Line (Integer'Image (Convert (J_Access))); Ada.Text_IO.Put_Line (Integer'Image (Unchecked_Convert (J_Access))); end Convert_Test;
How can we implement Conversion
? It turns out that discriminant records with defaults combined with aliasing do the trick:
function Conversion (S : Source) return Target is type Source_Wrapper is tagged record S : Source; end record; type Target_Wrapper is tagged record T : Target; end record; type Selector is (Source_Field, Target_Field); type Magic (Sel : Selector := Target_Field) is record case Sel is when Source_Field => S : Source_Wrapper; when Target_Field => T : Target_Wrapper; end case; end record; M : Magic; function Convert (T : Target_Wrapper) return Target is begin M := (Sel => Source_Field, S => (S => S)); return T.T; end Convert; begin return Convert (M.T); end Conversion;
When the inner function Convert
is called, the discriminant Sel
of M
has the value Target_Field
, thus the component M.T
can be dereferenced. The assignment statement in Convert
changes the discriminant and the value. But the source value S
is still reachable as an object of type Target
because the parameter T
aliases the component M.T
, so the return statement executes without raising an exception.
The two tagged records are used to force that the inner Convert
function receives its parameter by reference. Without them, an implementation would be free to pass the discriminant record Magic
by copy, and aliasing would not occur.
Our implementation lacks the full power of Ada.Unchecked_Conversion
because it does not support limited or unconstrained types. However, it is sufficient to break type safety.
Addendum: Robert A Duff noted that this was already known in the Ada 83 timeframe. According to paragraph 3.7.2(4) in the standard (Ada 95 Annotated Reference Manual with Technical Corrigendum 1, MITRE Corporation, 2000, retrieved 2011-04-30), the call to Convert
is erroneous on its own, and not the return statement which uses the stale reference.
Addendum (2015): A Rust variant (for the unsafe language extension) of this approach has been published as A Type Safety Hole in Unsafe Rust).
2011-04-30: published
2011-04-30: Add note that this was already known in the Ada 83 timeframe.
2012-09-11: Minor corrections.
2015-03-22: Add reference to Rust variant.