[PATCH] GCC 16.2 Ada: fix automatic Lock_Free protected-object cleanup

Summary

I reproduced a GCC/GNAT 16.2.0 Ada front-end defect on FreeBSD 15.1 amd64 that causes unbounded process-memory growth when short-lived simple protected objects are constructed repeatedly.

A protected type eligible for GNAT’s automatic Lock_Free implementation can be expanded with a native System.Tasking.Protected_Objects.Protection _object field and Initialize_Protection, then be marked lock-free later during protected-body analysis. Cleanup subsequently trusts the later Uses_Lock_Free state and omits Finalize_Protection even though the native _object was already created and initialized.

This is an upstream GCC compiler defect, but the visible runtime consequence was reproduced on FreeBSD and a small FreeBSD ports-style downstream patch is provided here so an Ada-enabled GCC 16.2 port can carry the fix without waiting for upstream action.

FreeBSD port scope

The tested compiler is an Ada-enabled GCC 16.2.0 build derived from the FreeBSD GCC port configuration/patch set.

The current official lang/gcc16 port does not enable the Ada front end, so I am not claiming that the current lang/gcc16 binary package itself is affected. The FreeBSD Ada ports based on older GCC releases were not investigated as part of this report, and no claim is made about them.

The FreeBSD GCC port patches used for the GCC 16.2 baseline do not modify the three GCC Ada sources involved in this defect (exp_ch7.adb, exp_ch9.adb, sem_ch9.adb).

Deterministic compiler symptom

For GCC/GNAT 16.2.0 the default automatic case produces:

case                 Initialize  Finalize  lock-free ops  Protection field
default (automatic)         yes        no   yes            present
Lock_Free => True            no        no   yes            absent
Lock_Free => False          yes       yes   no             present

The inconsistent default case is the bug.

Minimal source:

procedure Protected_Finalize is
   protected type Prot is
      procedure Touch;
   private
      Value : Integer := 0;
   end Prot;

   protected body Prot is
      procedure Touch is
      begin
         Value := Value + 1;
      end Touch;
   end Prot;

   Object : Prot;
begin
   Object.Touch;
end Protected_Finalize;

Compile with:

gcc16 -c -O0 -fdump-tree-original protected_finalize.adb

The unmodified GCC 16.2 tree dump contains system.tasking.protected_objects.initialize_protection but not system.tasking.protected_objects.finalize_protection.

FreeBSD runtime consequence

Using the standalone workload with 50 batches of 100 requests, the GCC/GNAT 16.2.0 baseline produced:

case                    constructions   RSS growth   VSZ growth
plain_eight                        0       +4 KiB       0 KiB
direct_protected_one           5,000     +628 KiB    +528 KiB
protected_one                  5,000     +628 KiB    +528 KiB
protected_eight               40,000    +5012 KiB   +5016 KiB
lock_free_eight              40,000       +4 KiB       0 KiB

Threads and file descriptors remained stable. On FreeBSD the native protected object path ultimately uses pthread_mutex_init / pthread_mutex_destroy, so the missing generated finalization is directly visible as native mutex-state memory growth.

Root cause

The GCC 16.2 front-end ordering is:

Expand_N_Protected_Type_Declaration
  -> Uses_Lock_Free is still False
  -> generate native Protection _object
Build_Record_Init_Proc
  -> generate Initialize_Protection
Analyze_Protected_Body
  -> automatically Set_Uses_Lock_Free
cleanup generation
  -> Is_Simple_Protected_Type tests not Uses_Lock_Free
  -> skip existing Finalize_Protection path

An LLDB trace confirms Make_Initialize_Protection is reached before the later Set_Uses_Lock_Free for the reproducer.

Fix

The fix makes Is_Simple_Protected_Type classify the type from the expanded representation that actually exists rather than the later mutable Uses_Lock_Free flag. It checks that the corresponding record was analyzed and contains _object before reusing Find_Protection_Type to confirm that the field is the normal System.Tasking.Protected_Objects.Protection type.

This keeps explicit Lock_Free => True unchanged because that representation has no _object field, and keeps protected types with entries on their existing path.

The FreeBSD ports-style patch is included below:

patch-gcc_ada_exp__ch7.adb

It applies from the GCC 16.2 source root with patch -p0.

Validation of the patch

The exact functional compiler hunk in the FreeBSD patch was built and tested in an isolated GCC/GNAT 16.2 compiler tree.

Results after patching:

default (automatic)   Initialize=yes  Finalize=yes  lock-free=yes
Lock_Free => True     Initialize=no   Finalize=no   lock-free=yes
Lock_Free => False    Initialize=yes  Finalize=yes  lock-free=no

The standalone FreeBSD runtime showed no construction-proportional growth for any case over the same workload:

plain_eight                RSS +4 KiB, VSZ 0 KiB
direct_protected_one       RSS +4 KiB, VSZ 0 KiB
protected_one              RSS +4 KiB, VSZ 0 KiB
protected_eight            RSS +4 KiB, VSZ 0 KiB
lock_free_eight            RSS +4 KiB, VSZ 0 KiB

A selected GCC Ada DejaGNU regression set completed with 20 expected passes, zero unexpected failures, and zero unresolved tests. Additional probes showed paired finalization restored for automatic protected subtypes, arrays, and heap objects; an entry-bearing protected-type tree dump was byte-identical before and after the patch.

Requested FreeBSD action

Please consider carrying the supplied source patch in an Ada-enabled GCC 16.2 port (for example a future GNAT 16 port or equivalent) until the issue is fixed in the compiler source used by that port.

I can provide the standalone runtime reproducer, raw measurements, compiler tree dumps, LLDB ordering trace, and DejaGNU summaries if they are useful for review. The full evidence is retained locally; the small source patch and this report are intended to be sufficient for the initial FreeBSD review.

Bugzilla fallback if requested by the maintainer

FreeBSD’s normal existing-port patch path is Bugzilla. If the Ada maintainers want this tracked there after selecting an affected/target port, use:

Product:   Ports & Packages
Component: Individual Port(s)
Summary:   [PATCH] lang/<maintainer-selected-port>: fix GCC 16.2 Ada protected cleanup

Attach patch-gcc_ada_exp__ch7.adb uncompressed. Do not substitute lang/gcc16 merely to fill the field: its current official build does not enable Ada. Use the exact target port chosen by the FreeBSD maintainers.

patch-gcc_ada_exp__ch7.adb:

--- gcc/ada/exp_ch7.adb.orig
+++ gcc/ada/exp_ch7.adb
@@ -5432,12 +5432,34 @@
    ------------------------------

    function Is_Simple_Protected_Type (T : Entity_Id) return Boolean is
+      Comp    : Entity_Id;
+      Rec_Typ : Entity_Id;
+
    begin
-      return
-        Is_Protected_Type (T)
-          and then not Uses_Lock_Free (T)
-          and then not Has_Entries (T)
-          and then Is_RTE (Find_Protection_Type (T), RE_Protection);
+      if not Is_Protected_Type (T) or else Has_Entries (T) then
+         return False;
+      end if;
+
+      Rec_Typ := Corresponding_Record_Type (T);
+
+      if No (Rec_Typ) or else not Analyzed (Rec_Typ) then
+         return False;
+      end if;
+
+      --  Base the decision on the representation that was actually built.
+      --  Automatic lock-free selection may happen after the corresponding
+      --  record and its initialization procedure have already been created.
+
+      Comp := First_Component (Rec_Typ);
+      while Present (Comp) loop
+         if Chars (Comp) = Name_uObject then
+            return Is_RTE (Find_Protection_Type (T), RE_Protection);
+         end if;
+
+         Next_Component (Comp);
+      end loop;
+
+      return False;
    end Is_Simple_Protected_Type;

    -------------------------------