[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;
-------------------------------