From b349bf669f0ecd699b59358323d5529487f2a855 Mon Sep 17 00:00:00 2001
From: Peter Geoghegan <pg@bowt.ie>
Date: Tue, 6 Oct 2026 13:32:38 -0400
Subject: [PATCH v2_master] Rescind unsafe deduplication support.

bpchar equality ignores trailing spaces, so equal values in a column
with no length can have different images.  Similarly, oidvector equality
intentionally pays no attention to the array lower bound field.  We
nevertheless allowed nbtree deduplication to be used with indexes built
with one of these opclass.  This could lead to an index-only scan that
returns the wrong value following a posting list split.

To fix, drop support function 4 from all three affected opclasses on
master and 19.  It isn't possible to change catalog contents on earlier
branches, so handle the problem there by explicitly teaching the
relevant support functions to reject the two bpchar opclasses only.  We
deliberately avoid changing anything about the oidvector opclass on
these earlier back branches, though.  The oidvector issue is benign in
practice, and it doesn't seem worth the trouble that it'd cause users
who routinely run pg_amcheck.  amcheck would otherwise flag affected
system catalog indexes (such as pg_proc_proname_args_nsp_index) as
containing a corrupt metapage, even though the underlying issue can't
cause any harm with idiomatic usage within system catalogs.

Author: Shihao Zhong <zhong950419@gmail.com>
Co-authored-by: Peter Geoghegan <pg@bowt.ie>
Reported-by: Ke <kehan5800@gmail.com>
Reported-by: Tom Lane <tgl@sss.pgh.pa.us>
Bug: #19749
Discussion: https://postgr.es/m/19749-d46adf7c7d910b07@postgresql.org
Backpatch-through: 14
---
 src/include/catalog/pg_amproc.dat        |  7 -----
 contrib/amcheck/verify_nbtree.c          | 16 +---------
 doc/src/sgml/btree.sgml                  | 14 ++++++---
 src/test/regress/expected/opr_sanity.out | 40 +++++++++++++-----------
 4 files changed, 32 insertions(+), 45 deletions(-)

diff --git a/src/include/catalog/pg_amproc.dat b/src/include/catalog/pg_amproc.dat
index 4a1efdbc8..27fb8cd2d 100644
--- a/src/include/catalog/pg_amproc.dat
+++ b/src/include/catalog/pg_amproc.dat
@@ -30,9 +30,6 @@
 { amprocfamily => 'btree/bpchar_ops', amproclefttype => 'bpchar',
   amprocrighttype => 'bpchar', amprocnum => '2',
   amproc => 'bpchar_sortsupport' },
-{ amprocfamily => 'btree/bpchar_ops', amproclefttype => 'bpchar',
-  amprocrighttype => 'bpchar', amprocnum => '4',
-  amproc => 'btvarstrequalimage' },
 { amprocfamily => 'btree/bytea_ops', amproclefttype => 'bytea',
   amprocrighttype => 'bytea', amprocnum => '1', amproc => 'byteacmp' },
 { amprocfamily => 'btree/bytea_ops', amproclefttype => 'bytea',
@@ -224,8 +221,6 @@
 { amprocfamily => 'btree/oidvector_ops', amproclefttype => 'oidvector',
   amprocrighttype => 'oidvector', amprocnum => '1',
   amproc => 'btoidvectorcmp' },
-{ amprocfamily => 'btree/oidvector_ops', amproclefttype => 'oidvector',
-  amprocrighttype => 'oidvector', amprocnum => '4', amproc => 'btequalimage' },
 { amprocfamily => 'btree/text_ops', amproclefttype => 'text',
   amprocrighttype => 'text', amprocnum => '1', amproc => 'bttextcmp' },
 { amprocfamily => 'btree/text_ops', amproclefttype => 'text',
@@ -273,8 +268,6 @@
 { amprocfamily => 'btree/bpchar_pattern_ops', amproclefttype => 'bpchar',
   amprocrighttype => 'bpchar', amprocnum => '2',
   amproc => 'btbpchar_pattern_sortsupport' },
-{ amprocfamily => 'btree/bpchar_pattern_ops', amproclefttype => 'bpchar',
-  amprocrighttype => 'bpchar', amprocnum => '4', amproc => 'btequalimage' },
 { amprocfamily => 'btree/money_ops', amproclefttype => 'money',
   amprocrighttype => 'money', amprocnum => '1', amproc => 'cash_cmp' },
 { amprocfamily => 'btree/money_ops', amproclefttype => 'money',
diff --git a/contrib/amcheck/verify_nbtree.c b/contrib/amcheck/verify_nbtree.c
index c5e1f9010..b2ae22fc9 100644
--- a/contrib/amcheck/verify_nbtree.c
+++ b/contrib/amcheck/verify_nbtree.c
@@ -329,24 +329,10 @@ bt_index_check_callback(Relation indrel, Relation heaprel, void *state, bool rea
 				 errmsg("index \"%s\" metapage has equalimage field set on unsupported nbtree version",
 						RelationGetRelationName(indrel))));
 	if (allequalimage && !_bt_allequalimage(indrel, false))
-	{
-		bool		has_interval_ops = false;
-
-		for (int i = 0; i < IndexRelationGetNumberOfKeyAttributes(indrel); i++)
-			if (indrel->rd_opfamily[i] == INTERVAL_BTREE_FAM_OID)
-			{
-				has_interval_ops = true;
-				break;
-			}
-
 		ereport(ERROR,
 				(errcode(ERRCODE_INDEX_CORRUPTED),
 				 errmsg("index \"%s\" metapage incorrectly indicates that deduplication is safe",
-						RelationGetRelationName(indrel)),
-				 has_interval_ops
-				 ? errhint("This is known of \"interval\" indexes last built on a version predating 2023-11.")
-				 : 0));
-	}
+						RelationGetRelationName(indrel))));
 
 	/* Check index, possibly against table it is an index on */
 	bt_check_every_level(indrel, heaprel, heapkeyspace, readonly,
diff --git a/doc/src/sgml/btree.sgml b/doc/src/sgml/btree.sgml
index 027361f20..78ba106bc 100644
--- a/doc/src/sgml/btree.sgml
+++ b/doc/src/sgml/btree.sgml
@@ -875,10 +875,16 @@ options(<replaceable>relopts</replaceable> <type>local_relopts *</type>) returns
    <itemizedlist>
     <listitem>
      <para>
-      <type>text</type>, <type>varchar</type>, and <type>char</type>
-      cannot use deduplication when a
-      <emphasis>nondeterministic</emphasis> collation is used.  Case
-      and accent differences must be preserved among equal datums.
+      <type>text</type> and <type>varchar</type> cannot use deduplication when
+      a <emphasis>nondeterministic</emphasis> collation is used.  Case and
+      accent differences must be preserved among equal datums.
+     </para>
+    </listitem>
+
+    <listitem>
+     <para>
+      <type>char</type> cannot use deduplication.  Trailing spaces must be
+      preserved among equal datums.
      </para>
     </listitem>
 
diff --git a/src/test/regress/expected/opr_sanity.out b/src/test/regress/expected/opr_sanity.out
index 67cae3978..907d63daa 100644
--- a/src/test/regress/expected/opr_sanity.out
+++ b/src/test/regress/expected/opr_sanity.out
@@ -2225,25 +2225,27 @@ LEFT JOIN pg_amproc AS amp ON amp.amprocfamily = opf.oid AND
 WHERE am.amname = 'btree' AND
     amp.amproc IS DISTINCT FROM 'btequalimage'::regproc
 ORDER BY 1, 2, 3;
-        proc        |  opfamily_name   |   opclass_name   |    opcintype     
---------------------+------------------+------------------+------------------
- btvarstrequalimage | bpchar_ops       | bpchar_ops       | character
- btvarstrequalimage | text_ops         | name_ops         | name
- btvarstrequalimage | text_ops         | text_ops         | text
- btvarstrequalimage | text_ops         | varchar_ops      | text
-                    | array_ops        | array_ops        | anyarray
-                    | float_ops        | float4_ops       | real
-                    | float_ops        | float8_ops       | double precision
-                    | interval_ops     | interval_ops     | interval
-                    | jsonb_ops        | jsonb_ops        | jsonb
-                    | multirange_ops   | multirange_ops   | anymultirange
-                    | numeric_ops      | numeric_ops      | numeric
-                    | range_ops        | range_ops        | anyrange
-                    | record_image_ops | record_image_ops | record
-                    | record_ops       | record_ops       | record
-                    | tsquery_ops      | tsquery_ops      | tsquery
-                    | tsvector_ops     | tsvector_ops     | tsvector
-(16 rows)
+        proc        |   opfamily_name    |    opclass_name    |    opcintype     
+--------------------+--------------------+--------------------+------------------
+ btvarstrequalimage | text_ops           | name_ops           | name
+ btvarstrequalimage | text_ops           | text_ops           | text
+ btvarstrequalimage | text_ops           | varchar_ops        | text
+                    | array_ops          | array_ops          | anyarray
+                    | bpchar_ops         | bpchar_ops         | character
+                    | bpchar_pattern_ops | bpchar_pattern_ops | character
+                    | float_ops          | float4_ops         | real
+                    | float_ops          | float8_ops         | double precision
+                    | interval_ops       | interval_ops       | interval
+                    | jsonb_ops          | jsonb_ops          | jsonb
+                    | multirange_ops     | multirange_ops     | anymultirange
+                    | numeric_ops        | numeric_ops        | numeric
+                    | oidvector_ops      | oidvector_ops      | oidvector
+                    | range_ops          | range_ops          | anyrange
+                    | record_image_ops   | record_image_ops   | record
+                    | record_ops         | record_ops         | record
+                    | tsquery_ops        | tsquery_ops        | tsquery
+                    | tsvector_ops       | tsvector_ops       | tsvector
+(18 rows)
 
 -- **************** pg_index ****************
 -- Look for illegal values in pg_index fields.
-- 
2.55.0

