Hi,

While working on the pg_get_domain_ddl() patch [1], I installed an
event trigger on ddl_command_end in test_setup.sql to automatically
round-trip DDL.  This triggered a UBSAN crash on CI [2] that turns out
to be a pre-existing bug since b488c580aef(?).


The DROP MAPPING code path in tsearchcmds.c unconditionally calls:

```
EventTriggerCollectAlterTSConfig(stmt, cfgId, NULL, 0);
```

Inside EventTriggerCollectAlterTSConfig(), this reaches:

```
command->d.atscfg.dictIds = palloc_array(Oid, ndicts);   /* ndicts=0 */
memcpy(command->d.atscfg.dictIds, dictIds, sizeof(Oid) * ndicts);
                                   ^^^^^^^ NULL
```

Under -fsanitize=undefined this triggers SIGABRT and crashes the server.

The bug has been latent for 11 years because without an active event
trigger, currentEventTriggerState is NULL and the function returns early
at the top, never reaching the memcpy.  The standard regression suite
never had an event trigger installed during the tsdicts test — until now.

Reproducer (crashes only under UBSAN) and patch attached:


This affects all branches back to 9.5 where b488c580aef landed.

[1] 
https://www.postgresql.org/message-id/CAPgqM1V4LW2qiDLPsusb7s0kYbSDJjH5Tt%2B-ZzVmPU7xV0TJNQ%40mail.gmail.com
[2] https://cirrus-ci.com/task/6170470552174592

--

Cheers,
Florin

EDB -- www.enterprisedb.com
From bc38f7016c868ff1be68214b4a134773cda214cb Mon Sep 17 00:00:00 2001
From: Florin Irion <[email protected]>
Date: Tue, 3 Mar 2026 17:41:23 +0100
Subject: [PATCH v1 1/2] Add regression test for
 EventTriggerCollectAlterTSConfig with NULL dictIds

The DROP MAPPING code path in tsearchcmds.c calls
EventTriggerCollectAlterTSConfig(stmt, cfgId, NULL, 0).  When an event
trigger on ddl_command_end is active, this reaches memcpy(dest, NULL, 0)
which is undefined behavior.  Under -fsanitize=undefined this crashes
the server.

Add a test that exercises this exact code path: a ddl_command_end event
trigger combined with ALTER TEXT SEARCH CONFIGURATION ... DROP MAPPING.

Bug latent since b488c580aef (2015).
---
 src/test/regress/expected/event_trigger.out | 15 +++++++++++++++
 src/test/regress/sql/event_trigger.sql      | 18 ++++++++++++++++++
 2 files changed, 33 insertions(+)

diff --git a/src/test/regress/expected/event_trigger.out 
b/src/test/regress/expected/event_trigger.out
index 16e4530708c..1f37f4c3d5a 100644
--- a/src/test/regress/expected/event_trigger.out
+++ b/src/test/regress/expected/event_trigger.out
@@ -817,3 +817,18 @@ NOTICE:  DROP POLICY dropped policy
 CREATE POLICY pguc ON event_trigger_test USING (FALSE);
 SET event_triggers = 'off';
 DROP POLICY pguc ON event_trigger_test;
+-- Test that EventTriggerCollectAlterTSConfig handles DROP MAPPING
+-- with ndicts=0, dictIds=NULL (the DROP MAPPING code path always
+-- passes NULL,0).  Previously crashed under UBSAN due to
+-- memcpy(dest, NULL, 0) being undefined behavior.
+CREATE FUNCTION noop_event_trigger() RETURNS event_trigger
+LANGUAGE plpgsql AS $$ BEGIN END; $$;
+CREATE EVENT TRIGGER noop_event_trigger ON ddl_command_end
+    EXECUTE FUNCTION noop_event_trigger();
+SET event_triggers = 'on';
+CREATE TEXT SEARCH CONFIGURATION evttrig_tscfg (COPY = pg_catalog.simple);
+ALTER TEXT SEARCH CONFIGURATION evttrig_tscfg
+    DROP MAPPING FOR word;
+DROP TEXT SEARCH CONFIGURATION evttrig_tscfg;
+DROP EVENT TRIGGER noop_event_trigger;
+DROP FUNCTION noop_event_trigger;
diff --git a/src/test/regress/sql/event_trigger.sql 
b/src/test/regress/sql/event_trigger.sql
index c613c0cfd43..d955b3d0abe 100644
--- a/src/test/regress/sql/event_trigger.sql
+++ b/src/test/regress/sql/event_trigger.sql
@@ -638,3 +638,21 @@ DROP POLICY pguc ON event_trigger_test;
 CREATE POLICY pguc ON event_trigger_test USING (FALSE);
 SET event_triggers = 'off';
 DROP POLICY pguc ON event_trigger_test;
+
+-- Test that EventTriggerCollectAlterTSConfig handles DROP MAPPING
+-- with ndicts=0, dictIds=NULL (the DROP MAPPING code path always
+-- passes NULL,0).  Previously crashed under UBSAN due to
+-- memcpy(dest, NULL, 0) being undefined behavior.
+CREATE FUNCTION noop_event_trigger() RETURNS event_trigger
+LANGUAGE plpgsql AS $$ BEGIN END; $$;
+CREATE EVENT TRIGGER noop_event_trigger ON ddl_command_end
+    EXECUTE FUNCTION noop_event_trigger();
+SET event_triggers = 'on';
+
+CREATE TEXT SEARCH CONFIGURATION evttrig_tscfg (COPY = pg_catalog.simple);
+ALTER TEXT SEARCH CONFIGURATION evttrig_tscfg
+    DROP MAPPING FOR word;
+
+DROP TEXT SEARCH CONFIGURATION evttrig_tscfg;
+DROP EVENT TRIGGER noop_event_trigger;
+DROP FUNCTION noop_event_trigger;
-- 
2.45.1

From 04344b57c717271a372d1e6335309e0fe53458b6 Mon Sep 17 00:00:00 2001
From: Florin Irion <[email protected]>
Date: Tue, 3 Mar 2026 17:44:18 +0100
Subject: [PATCH v1 2/2] Fix UBSAN crash in EventTriggerCollectAlterTSConfig

Guard the palloc_array + memcpy with an ndicts > 0 check.  The command
struct is already palloc0'd so dictIds defaults to NULL.
---
 src/backend/commands/event_trigger.c | 7 +++++--
 1 file changed, 5 insertions(+), 2 deletions(-)

diff --git a/src/backend/commands/event_trigger.c 
b/src/backend/commands/event_trigger.c
index 028f9e2de90..2898967fa67 100644
--- a/src/backend/commands/event_trigger.c
+++ b/src/backend/commands/event_trigger.c
@@ -2004,8 +2004,11 @@ 
EventTriggerCollectAlterTSConfig(AlterTSConfigurationStmt *stmt, Oid cfgId,
        command->in_extension = creating_extension;
        ObjectAddressSet(command->d.atscfg.address,
                                         TSConfigRelationId, cfgId);
-       command->d.atscfg.dictIds = palloc_array(Oid, ndicts);
-       memcpy(command->d.atscfg.dictIds, dictIds, sizeof(Oid) * ndicts);
+       if (ndicts > 0)
+       {
+               command->d.atscfg.dictIds = palloc_array(Oid, ndicts);
+               memcpy(command->d.atscfg.dictIds, dictIds, sizeof(Oid) * 
ndicts);
+       }
        command->d.atscfg.ndicts = ndicts;
        command->parsetree = (Node *) copyObject(stmt);
 
-- 
2.45.1

Reply via email to