https://gcc.gnu.org/bugzilla/show_bug.cgi?id=126014
Bug ID: 126014
Summary: DSA: RACW receiver stub dispatches interface primitive
via wrong DT slot (crashes before body)
Product: gcc
Version: 16.1.1
URL: https://codeberg.org/charlie5/aDSA/src/branch/main/exa
mples/asyncfn5
Status: UNCONFIRMED
Keywords: wrong-code
Severity: normal
Priority: P3
Component: ada
Assignee: unassigned at gcc dot gnu.org
Reporter: rodakay5 at gmail dot com
CC: dkm at gcc dot gnu.org
Target Milestone: ---
Host: x86_64-pc-linux-gnu
Target: x86_64-pc-linux-gnu
Build: x86_64-pc-linux-gnu
Created attachment 64881
--> https://gcc.gnu.org/bugzilla/attachment.cgi?id=64881&action=edit
asyncfn5 DSA reproducer + exp_dist.adb fix + stock-compiler proof and -gnatD
evidence
GNAT bug report
===============
Title: DSA: RACW receiver stub (gnatzr) dispatches to wrong primary-DT slot
when concrete type has non-interface own primitives before overriding
declarations
Summary
-------
When a concrete type that implements a Remote_Types interface (RACW dispatch)
also has non-interface own primitive declarations that appear BEFORE the
interface-overriding declarations, the GNAT-generated receiver stub (produced
by gcc -gnatzr from the interface spec) dispatches to the wrong dispatch
table slot and crashes (STORAGE_ERROR / SIGSEGV) before the user body is
entered.
The slot index hardcoded in the receiver stub is computed from the interface
type's own-primitive count and is correct only when the concrete type's DT
has the interface's prims starting at exactly the same slot — which fails
whenever the concrete type has extra own prims beforehand.
Affected: reproduced and fixed on FSF GNAT 16.1.1 (gcc 16, trunk, 20260625).
The receiver-stub builder (exp_dist.adb GARLIC_Support.Build_Subprogram_
Receiving_Stubs) is structurally the same on the 14.x / 15.x branches, so they
are very likely affected too, but that was not verified here. This report
concerns the GARLIC (stream-based) personality, which our ZMQ PCS uses; the
PolyORB "Any" personality was not investigated (the same application is
reported to run under PolyORB without this crash).
How to verify (please read before triaging as "cannot reproduce")
-----------------------------------------------------------------
This is a Distributed Systems Annex (Annex E) code-generation bug, so it only
manifests with a PCS in place. Stock FSF GNAT ships
System.Partition_Interface.DSA_Implementation = No_DSA, under which exp_dist
generates no stubs at all -- a plain build of the reproducer silently behaves
non-distributed and shows nothing. Generating the faulty stub requires a
runtime that selects a PCS personality (here, GARLIC_DSA); the attached
ZMQ-backed PCS provides one.
You do NOT need to run it to confirm the bug:
* Static evidence (no PCS, no run) -- the attached -gnatD dumps. In
world_types.ads.dg the receiver for the interface primitive Sprites
(SubpId 21) hardcodes ...itemH!(tag).all(22); in world_final.ads.dg the
concrete type's primary dispatch table has register_mirror at slot 22 and
Sprites at slot 26 -- so the receiver dispatches to the wrong primitive.
* Stock-compiler demonstration -- prove_fix.adb (build_prove_fix.sh) builds
and runs with a plain gcc/gnat, no PCS or libraries, and shows that
dispatching the interface primitive through the primary view (what the stub
does today) reaches the wrong primitive, while dispatching through the
interface view (what the patch does, via Ada.Tags.Displace) reaches the
real body.
Full end-to-end reproduction with the PCS is in the "To reproduce" section
below; the server raises STORAGE_ERROR before the user body runs.
Minimal reproducer
------------------
Location: examples/asyncfn5/ (self-contained; builds with build_asyncfn5.sh)
Type hierarchy (the relevant structure):
-- Interfaces (Remote_Types):
package Iface_Subject -- 11 abstract prims
package Iface_Observer -- 6 abstract prims
package World_Types -- 8 own abstract prims incl. Sprites
-- Concrete chain (server-side only):
World_Base.Object new Iface_Subject.Item and Iface_Observer.Item
-- implements all 17 inherited prims (slots 1..17)
|
World_Mid.Object abstract new World_Base.Object and World_Types.Item
| -- HERE: 4 extra non-interface own prims declared
BEFORE
| -- the 8 overriding declarations for World_Types
v
World_Final.Object new World_Mid.Object with null record
-- the actually-allocated type; inherits everything
-- RCI (factory):
World_Svc Remote_Call_Interface
function Make (Initial : Integer) return World_Types.View;
-- returns World_Types.View pointing at
World_Final.Object
World_Mid.ads (the critical ordering — see source):
type Object is abstract new World_Base.Object and World_Types.Item
with null record;
-- 4 NON-INTERFACE own prims (slots 18..21 in primary DT):
function World_Id (Self : in Object) return Integer;
function World_Age (Self : in Object) return Float;
procedure World_Age_Is (Self : in out Object; Age : in Float);
procedure World_Destroy (Self : in out Object);
-- World_Types overrides (slots 22..29 in primary DT, shifted by 4):
overriding procedure Register_Mirror (...); -- slot 22
overriding procedure Deregister_Mirror (...); -- slot 23
overriding function Graphics_Models (...); -- slot 24
overriding function Physics_Models (...); -- slot 25
overriding function Sprites (...); -- slot 26 <-- ACTUALLY HERE
overriding procedure Motion_Updates_Are(...); -- slot 27
overriding procedure Kick_Sprite (...); -- slot 28
overriding function Rich_Sprites (...); -- slot 29
The primary DT of World_Final.Object has 29 slots (confirmed by -gnatD dump).
What GNAT generates (gnatzr on World_Types.ads)
------------------------------------------------
The gnatzr-generated receiver for Sprites (SubpId 21, 0-indexed) calls:
world_types__F268s__B1240s__T1243s!(
world_types__itemH!(
world_types__view!(system__address?(P265s)).all._tag).all(22))
.all(world_types__view!(system__address?(P265s)).all)
Slot 22 is hardcoded. It is derived from:
11 (Subject) + 6 (Observer) + 4 (own prims of World_Types before Sprites)
= 21 prims before Sprites, so prim #22 (1-indexed) => .all(22).
This is correct for asyncfn4 (no extra prims), where slot 22 = Sprites.
This is WRONG for asyncfn5, where slot 22 = Register_Mirror.
Calling Register_Mirror
(procedure (Self : access Object; Mirror : Integer; Mirror_Obs : View))
with the Sprites ABI
(function (Self : in out Object) return Sprite_Pairs)
corrupts the stack => SIGSEGV => STORAGE_ERROR.
Confirmed by -gnatD dump of World_Final.ads
------------------------------------------
world_final__objectT.prims_ptr = (
[1] world_base__name_s,
[2] world_base__next_seq,
... (slots 1..17: Subject 11 + Observer 6 inherited from World_Base)
[18] world_mid__world_id,
[19] world_mid__world_age,
[20] world_mid__world_age_is,
[21] world_mid__world_destroy,
[22] world_mid__register_mirror, <- gnatzr calls .all(22) for Sprites
[23] world_mid__deregister_mirror,
[24] world_mid__graphics_models,
[25] world_mid__physics_models,
[26] world_mid__sprites, <- actually here, never reached
[27] world_mid__motion_updates_are,
[28] world_mid__kick_sprite,
[29] world_mid__rich_sprites);
Observed behavior
-----------------
Server raises STORAGE_ERROR before the Sprites body is ever entered.
Client re-raises the exception. Confirmed: the diagnostic Put_Line at the
start of the Sprites body body ("THIS LINE SHOULD NOT APPEAR") never prints.
Expected behavior
-----------------
The receiver stub should call the correct function (slot 26,
world_mid__sprites)
rather than using a hardcoded slot number that assumes the concrete type has no
extra own primitives before its interface overrides.
The correct fix is for the receiver stub to dispatch through the World_Types
interface's secondary dispatch table (which contains thunks that map each
interface primitive to its actual slot in the concrete type's primary DT),
rather than computing and hardcoding a primary-DT slot offset from the
interface's own-prim position.
Regression test
---------------
asyncfn4 (same structure, but without the 4 extra non-interface prims) PASSES.
This proves the PCS runtime is correct; the bug is purely in gnatzr code-gen.
To reproduce
------------
1. Install a DSA-capable PCS that uses the GARLIC (stream-based) personality;
the ZMQ PCS from this repo works. (See "How to verify" above for why a PCS
is needed and how to confirm the bug without running it.)
2. cd examples/asyncfn5
3. bash build_asyncfn5.sh # builds server_main5 + client_main5
4. pcs_nameserver &
5. ./obj-server5/server_main5 &
6. ./obj-client5/client_main5
7. Observe: client prints "[client] calling Sprites ..." then
"raised STORAGE_ERROR : s-intman.adb:136 explicit raise".
The server log is empty (body never entered).
Alternatively: run bash build_asyncfn4.sh and obj-server4/server_main4 &
then obj-client4/client_main4 to see the PASS case (asyncfn4 has no extra
own prims before overriding declarations).
Proposed fix
------------
See 0001-DSA-displace-RACW-controlling-actual-to-interface-view.patch
(against gcc/ada/exp_dist.adb).
Root cause: in Build_Subprogram_Receiving_Stubs (the GARLIC variant), the
controlling object for the dispatching call is rebuilt from the marshalled
address with a bare
Unchecked_Convert_To (RACW_Type, OK_Convert_To (Address, Object))
The marshalled value is the object's BASE (primary) address, so this access
value is the primary view. When RACW_Type designates an interface, the
ensuing dispatching call reads the object's primary tag and indexes it with
the primitive's interface-relative position -- which is only valid when the
concrete type adds no primitives ahead of its interface overrides.
The fix wraps the address in Ada.Tags.Displace (addr, Item'Tag) when the
designated type is an interface, so the access value is the interface
(secondary) view and the dispatch goes through the interface's dispatch
table. This is the same RTL routine (RE_Displace) the front end already
inserts for an ordinary access conversion to an interface class-wide type.
Validation without rebuilding GCC: prove_fix.adb (build_prove_fix.sh)
reproduces both code paths by hand on a local World_Final.Object. It builds
with a STOCK gnat compiler -- no PCS, no custom RTS, no libzmq -- because the
discrepancy is pure Ada 2005 interface dispatch (primary view vs interface
view); the DSA is only how the un-displaced pointer arises in practice. The
displaced (patched) path enters the real Sprites body and returns 3 pairs;
the raw-address (current) path does not enter Sprites and returns garbage:
[PATCHED path] interface view (compiler-displaced), dispatch Sprites:
[server/World_Mid] Sprites body entered ...
-> Sprites returned 3 pairs. CORRECT: reached the real Sprites body.
[BUGGY path] primary address, NO displace, dispatch Sprites:
-> returned 996358900 pairs (unsound; any value here is luck).
Scope: this patch addresses the GARLIC (stream-based) receiving-stub builder
(GARLIC_Support.Build_Subprogram_Receiving_Stubs), the one the reproducer
exercises. The PolyORB receiving-stub builder
(PolyORB_Support.Build_Subprogram_Receiving_Stubs) uses a structurally similar
raw-address conversion (Get_Local_Address -> Unchecked_Convert_To (RACW_Type,
Addr)), so it may be susceptible to the same issue; however, the same
application is reported to run under a PolyORB-based PCS without this crash, so
PolyORB is left out of scope and not investigated here.
Status: the patch has been built into a full GCC and verified end-to-end -- it
fixes the crash both in the asyncfn5 reproducer and in the real application the
bug was found in. It is validated by prove_fix on a stock compiler as above.
GNAT version
------------
Output of gcc --version:
gcc (GCC) 16.1.1 20260625 (target x86_64-pc-linux-gnu)
Verified on this version; 14.x/15.x likely affected by inspection, not
tested.
Related
-------
This bug was discovered while implementing a ZMQ-backed PCS for Ada's
Distributed Systems Annex (Annex E) as a replacement for PolyORB. It
manifests in the charlie5/lace+gel ("arcana") application where gel.World.Item
declares many non-interface own primitives (Id, Age, Space, local_Observer,
local_Subject, etc.) before its gel.remote.World.item overrides, causing the
gel.remote.world.Sprites receiver to dispatch to an unrelated function.