[PATCH] GCC 16.2 Ada:修复自动 Lock_Free 受保护对象的清理遗漏

摘要

我在 FreeBSD 15.1 amd64 上复现了 GCC/GNAT 16.2.0 Ada 前端的一个缺陷。 当反复构造生命周期很短的简单受保护对象时,该缺陷会导致进程内存无限增长。

一个符合 GNAT 自动 Lock_Free 实现条件的受保护类型,可能先被展开为包含原生 System.Tasking.Protected_Objects.Protection _object 字段和 Initialize_Protection 的表示形式,随后在分析受保护体时才被标记为 lock-free。之后生成清理代码时会信任这个较晚确定的 Uses_Lock_Free 状态, 因此即使原生 _object 已经被创建并初始化,也会省略 Finalize_Protection

这是 GCC 上游编译器的缺陷,但其可见的运行时后果是在 FreeBSD 上复现的。 这里同时提供一个小型 FreeBSD ports 风格的下游补丁,使启用 Ada 的 GCC 16.2 port 无需等待上游处理即可包含此修复。

FreeBSD port 范围

测试所用编译器是一个启用了 Ada 的 GCC 16.2.0 构建,它基于 FreeBSD GCC port 的配置和补丁集。

当前官方 lang/gcc16 port 并未启用 Ada 前端,因此我并不是在声称当前 lang/gcc16 二进制软件包本身会受到影响。本报告没有调查基于较早 GCC 版本的 FreeBSD Ada ports,也不对它们是否受影响作任何声明。

用于 GCC 16.2 基线的 FreeBSD GCC port 补丁没有修改与该缺陷有关的三个 GCC Ada 源文件(exp_ch7.adbexp_ch9.adbsem_ch9.adb)。

可稳定复现的编译器症状

对于 GCC/GNAT 16.2.0,默认的自动选择情形会产生:

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

不一致的默认情形就是这个 bug。

最小源代码如下:

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;

使用以下命令编译:

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

未经修改的 GCC 16.2 tree dump 中包含 system.tasking.protected_objects.initialize_protection,但不包含 system.tasking.protected_objects.finalize_protection

FreeBSD 运行时后果

使用每批 100 个请求、共 50 批的独立工作负载时,GCC/GNAT 16.2.0 基线产生:

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

线程数和文件描述符数保持稳定。在 FreeBSD 上,原生受保护对象路径最终会使用 pthread_mutex_init / pthread_mutex_destroy,因此应生成的 finalization 一旦缺失, 就会直接表现为原生互斥量状态所占内存的增长。

根本原因

GCC 16.2 前端的处理顺序如下:

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

LLDB 跟踪确认,对于该复现程序,Make_Initialize_Protection 会在较晚的 Set_Uses_Lock_Free 之前到达。

修复

该修复让 Is_Simple_Protected_Type 根据实际已经构造出的展开表示来分类类型, 而不是依据之后仍可能发生变化的 Uses_Lock_Free 标志。它先检查对应 record 已经完成分析并且包含 _object,然后复用 Find_Protection_Type 来确认该字段 是普通的 System.Tasking.Protected_Objects.Protection 类型。

显式 Lock_Free => True 的表示中没有 _object 字段,因此其行为保持不变; 带 entry 的受保护类型也继续沿用原来的路径。

FreeBSD ports 风格的补丁包含在下面:

patch-gcc_ada_exp__ch7.adb

可在 GCC 16.2 源码根目录中使用 patch -p0 应用。

补丁验证

FreeBSD 补丁中实际生效的那段编译器修改,已在隔离的 GCC/GNAT 16.2 编译器树中完成构建和测试。

补丁后的结果为:

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

在相同工作负载下,FreeBSD 上的独立运行测试中,各种情形都没有出现与构造次数 成比例的增长:

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

选定的一组 GCC Ada DejaGNU 回归测试完成,其中 20 项测试按预期通过、0 项意外 失败、0 项 unresolved 测试。额外检查表明,在自动选择情形下,受保护 subtype、数组和 堆对象的成对 finalization 已恢复;一个带 entry 的受保护类型,其 tree dump 在 补丁前后逐字节完全相同。

请求 FreeBSD 采取的措施

在该问题被其所使用的编译器源码修复之前,请考虑在启用了 Ada 的 GCC 16.2 port(例如未来的 GNAT 16 port 或等价 port)中加入这里提供的源码补丁。

如果有助于审查,我可以提供独立运行时复现程序、原始测量数据、编译器 tree dump、LLDB 顺序跟踪以及 DejaGNU 摘要。完整证据保留在本地;这个小型源码补丁 和本报告旨在足以支持 FreeBSD 的初步审查。

如果 maintainer 要求使用 Bugzilla

FreeBSD 对现有 port 提交补丁的常规路径是 Bugzilla。如果 Ada maintainer 在选定受影响或目标 port 后希望在那里跟踪此问题,可使用:

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

请以未压缩形式附上 patch-gcc_ada_exp__ch7.adb。不要只是为了填写该字段而用 lang/gcc16 代替:它当前的官方构建并未启用 Ada。应使用 FreeBSD maintainer 所选择的确切目标 port。

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;

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