Subject: Fw:Re: Fw: gbt_var_consistent in contrib/btree_gist/btree_utils_var.c has internal-node type confusion on the <> strategy, bypassing exclusion constraints

From: 王跃林 <[email-masked]>
Date: Tue, 16 Jun 2026 19:29:35 +0800 (GMT+08:00)

王跃林
[email-masked]

Forwarded message:
From：Noah Misch <[email-masked]>Date：2026-06-13 08:29:28(中国 (GMT+08:00))To：王跃林<[email-masked]>Cc：security <[email-masked]>Subject：Re: Fw: gbt_var_consistent in contrib/btree_gist/btree_utils_var.c has internal-node type confusion on the <> strategy, bypassing exclusion constraintsOn Mon, Jun 08, 2026 at 11:24:02PM +0800, 王跃林 wrote:
> gbt_var_node_truncate (btree_utils_var.c:214) truncates internal node keys to a common-prefix length. The resulting bytea can have VARSIZE anywhere from 4 upward. When the truncated VARSIZE is below 8 and that key reaches bit_cmp via the buggy BtreeGistNotEqual branch, bytelen becomes negative. Passed to memcmp as size_t, that is several GB. ASan catches it as negative-size-param. A production build without ASan will eventually SEGV when the read crosses an unmapped page.

Got it.  That doesn't qualify as a vuln per
https://www.postgresql.org/support/security/:

  The PostgreSQL Security Team typically does not consider a denial-of-service
  on a PostgreSQL server from an authenticated, valid SQL statement to be a
  security vulnerability. A denial-of-service issue of this nature could still
  be a bug, and we encourage you to report it on the Report a Bug page.

If nobody objects by 2026-06-16T00:00+0000, please report the bug to
[email-masked].

---

From: Ayush Tiwari <[email-masked]>
Date: Thu, 18 Jun 2026 00:49:03 +0530

Hi,

On Tue, 16 Jun 2026 at 16:59, 王跃林 <[email-masked]> wrote:

> 0800, 王跃林 wrote:
> Thanks for the report!

In gbt_var_consistent() (contrib/btree_gist/btree_utils_var.c), the <>
(BtreeGistNotEqual) branch is the only strategy that does not distinguish
leaf from internal pages. It applies the type's equality function directly
to the stored key on both bounds:

  retval = !(tinfo->f_eq(query, key->lower, ...) &&
           tinfo->f_eq(query, key->upper, ...));

That seems to be the problem: on internal pages those keys are
truncated (and for bit/varbit the length header is gone too), so
they aren't really valid values to hand to the type's equality
function.  Hence the negative length reaching memcmp().

Would it make sense to just have <> follow the same leaf-vs-internal
pattern as the other strategies; negate equality on a leaf, and
otherwise recurse?  An internal page can always contain some
non-equal value, so I don't think we lose anything by descending.
I put together a small patch along those lines (plus a bit(8) test
that goes through internal pages); it's attached if it's useful.
(PS. The test exercises the path but passes on a plain build; the OOB
read shows up under ASan (negative-size-param).)

I didn't touch the fixed-size path in btree_utils_num.c, since its
internal keys look like they're exact lower/upper values, but
please correct me if I'm missing something there.

Regards,
Ayush

---

From: 王跃林 <[email-masked]>
Date: Thu, 18 Jun 2026 13:24:05 +0800 (GMT+08:00)

The analysis and fix look correct. The BtreeGistNotEqual branch is the only strategy that bypasses the is_leaf check and passes potentially truncated internal-node keys directly to f_eq, which is unsafe for types like bit and varbit that require a minimum valid header size. Returning true for internal nodes is the right conservative choice and is consistent with how the other strategies handle the internal-node case.

 王跃林
[email-masked]

Original:
From：Ayush Tiwari <[email-masked]>Date：2026-06-18 03:19:03(中国 (GMT+08:00))To：王跃林<[email-masked]>Cc：pgsql-bugs <[email-masked]>Subject：Re: Fw:Re: Fw: gbt_var_consistent in contrib/btree_gist/btree_utils_var.c has internal-node type confusion on the <> strategy, bypassing exclusion constraintsHi,

Thanks for the report!

Regards,
Ayush

---

From: Tom Lane <[email-masked]>
Date: Thu, 02 Jul 2026 10:38:16 -0400

=?UTF-8?B?546L6LeD5p6X?= <[email-masked]> writes:
> The analysis and fix look correct. The BtreeGistNotEqual branch is
> the only strategy that bypasses the is_leaf check and passes
> potentially truncated internal-node keys directly to f_eq, which is
> unsafe for types like bit and varbit that require a minimum valid
> header size. Returning true for internal nodes is the right
> conservative choice and is consistent with how the other strategies
> handle the internal-node case.

None of this passes the smell test for me.  If it's unsafe to
call the type's f_eq function on a truncated key, how is it
any safer to call the f_cmp function?  IOW, why aren't *all* the
cases in gbt_var_consistent() broken?

It looks to me like gbt_var_node_truncate adjusts the length words
of the truncated keys so that they are still valid, just shorter.
Or at least it's trying to.  That works fine for text and bytea,
but it's not fine for bit/varbit because (a) it fails to update the
"bit_len" field that follows the length word, and (b) the common
prefix length selection logic doesn't know that it mustn't truncate
away any part of the bit_len field.  So my own thought about fixing
this is that type bit needs a custom truncation method.

It might well be that gbt_var_consistent's not-equal case is
broken too, but this discussion hasn't established that IMO.
(I continue to regret that we ever accepted such underdocumented code.
I think we ought to reverse-engineer a comment explaining what
gbt_var_consistent is doing, eg, why are all of the tests seemingly
reversed?)

			regards, tom lane

---

From: Ayush Tiwari <[email-masked]>
Date: Thu, 2 Jul 2026 21:13:30 +0530

Hi,

> cases in gbt_var_consistent() broken?

I think the difference is that for bit/varbit f_eq and f_cmp aren't the
same kind of function.  f_eq is biteq (reads bit_len, does VARSIZE-8),
while f_cmp is byteacmp (plain byte-wise, only needs the varlena
header).  On internal pages every other strategy goes through
f_cmp/byteacmp, and only the <> branch reaches f_eq.  So I believe
that's why the byte-wise cases stay safe on a truncated key and <>
doesn't.

> this is that type bit needs a custom truncation method.

That was my first assumption too, but it looks like the node key has
no bit_len to preserve: gbt_bit_l2n/gbt_bit_xfrm seem to drop it and
keep only the raw bits before truncation runs.  If so, a custom
truncation may not help, since the crash seems to come from <> calling
biteq rather than a damaged bit_len.  Though I'm not fully sure I'm
reading the l2n step right.

> gbt_var_consistent is doing, eg, why are all of the tests seemingly

Agreed, we can add a comment there.

Regards,
Ayush

---

From: Tom Lane <[email-masked]>
Date: Thu, 02 Jul 2026 12:54:17 -0400

Ayush Tiwari <[email-masked]> writes:
>> any safer to call the f_cmp function?

> header).

Oh!  You are right, the internal keys are *not* valid bit/varbit
values, just bytea.  The level of non-documentation in this code
is staggering.

So the actual API contract here is that f_eq etc. are to be used only
on leaf keys, while f_cmp is to be used only on internal keys.  It's
not too surprising that whoever added NotEqual support didn't know
that, it being documented nowhere.

It kind of looks actually like there's an expectation that all
internal keys are effectively bytea's; note the naming of
gbt_bytea_pf_match, despite the fact that it's applied to all
types under the purview of btree_utils_var.c.

Anyway, I think part of our work here has got to be to raise the
level of documentation of this code.  I've had it with having
to reverse-engineer details as fundamental as these.

			regards, tom lane

---

From: Ayush Tiwari <[email-masked]>
Date: Thu, 2 Jul 2026 23:16:04 +0530

Hi,

> to reverse-engineer details as fundamental as these.

I've added some documentation in code, patch attached.

Please help reviewing if it looks good or should we be adding
something more.

Regards,
Ayush

---

From: Tom Lane <[email-masked]>
Date: Thu, 02 Jul 2026 19:02:17 -0400

> I've added some documentation in code, patch attached.

I had something considerably more ambitious in mind, so I had a
go at that --- and was glad that I did so, because the review
turned up an additional bug as well as some other opportunities
for improvement.

v3-0001 is a documentation-improvement patch.  I only touched
the files associated with btree_utils_var.c.  Maybe it's worth
doing something similar for the btree_utils_num.c group, but
I have the impression that those are way simpler.

v3-0002 is your patch, editorialized a bit.  I left out the
test case because it seemed fairly expensive for something
that won't expose a bug in normal builds.  (BTW, I wonder
if rather than documenting the comparisons as being reversed,
we should just flip them all.)

v3-0003 fixes a bug I identified: index builds that use the
gbt_bit_ssup_cmp infrastructure will sort the entries in a
way that doesn't match the bit types' actual ordering,
leading to what's probably a very inefficient index.

v3-0004 and v3-0005 clean up some things that seem like
dead code to me.

0003 through 0005 could use more eyeballs on them.

Also, it occurs to me after another look at 0002 that what was
perhaps intended was something like this for the non-leaf case:

            if (is_leaf)
                retval = !(tinfo->f_eq(query, key->lower, collation, flinfo));
            else
                retval = tinfo->trnc ||
                    !(tinfo->f_cmp(query, key->lower, collation, flinfo) == 0 &&
                      tinfo->f_cmp(query, key->upper, collation, flinfo) == 0);

That is, if we're looking at non-truncated keys and lower == upper,
then we know that all the keys below this node are exactly that
value, so we can avoid descending if that value is equal to the query.
Most of the time this would not pay off in any savings, but if you
had an index on a column with only a few values, maybe there would be
leaf pages like that?  I would think that the planner would avoid
choosing to implement a <> query with an indexscan unless there was
a pretty large fraction of the table that <> would reject, so maybe
this actually is worth doing.

			regards, tom lane

From 16f010e5853ee0cc9baf4e44753eda04c6fabe32 Mon Sep 17 00:00:00 2001
From: Tom Lane <[email-masked]>
Date: Thu, 2 Jul 2026 16:51:35 -0400
Subject: [PATCH v3 1/5] Reverse-engineer some documentation for btree_gist's
 varlena modules.

There were a number of rather subtle points about the behavior of
this code, which its original authors did not deign to document.
Try to improve that.  In particular, explain how internal and leaf
keys can differ and what the restrictions are on that.

This work arose from trying to fix some bugs, and in the process
I believe I've identified some more, but this patch does not attempt
to fix anything, only document it.  I did make a few purely cosmetic
code changes, such as removing dead (and confusing!) initializations
of variables and choosing more appropriate types for some pointers.

Author: Tom Lane <[email-masked]>
Discussion: https://postgr.es/m/[email-masked]
---
 contrib/btree_gist/btree_bit.c       | 51 ++++++++++++++++++++------
 contrib/btree_gist/btree_bytea.c     |  4 +-
 contrib/btree_gist/btree_numeric.c   |  9 ++++-
 contrib/btree_gist/btree_text.c      | 12 +++++-
 contrib/btree_gist/btree_utils_var.c | 55 ++++++++++++++++++++--------
 contrib/btree_gist/btree_utils_var.h | 38 ++++++++++++++++++-
 6 files changed, 136 insertions(+), 33 deletions(-)

diff --git a/contrib/btree_gist/btree_bit.c b/contrib/btree_gist/btree_bit.c
index 2b9c18a586f..dcfe6ed17e4 100644
--- a/contrib/btree_gist/btree_bit.c
+++ b/contrib/btree_gist/btree_bit.c
@@ -1,5 +1,11 @@
 /*
  * contrib/btree_gist/btree_bit.c
+ *
+ * Support for bit and varbit types (which act the same for our purposes).
+ *
+ * Leaf-page keys are bit/varbit values, but internal-page keys are just
+ * bytea values containing the first N bytes of the represented bitstring.
+ * (In particular, they lack the bit_len field of a bit/varbit Datum.)
  */
 #include "postgres.h"
 
@@ -63,6 +69,10 @@ gbt_bitlt(const void *a, const void *b, Oid collation, FmgrInfo *flinfo)
 											PointerGetDatum(b)));
 }
 
+/*
+ * Notice we use byteacmp here, not bitcmp as you might expect.
+ * That's because internal-page keys are bytea.
+ */
 static int32
 gbt_bitcmp(const void *a, const void *b, Oid collation, FmgrInfo *flinfo)
 {
@@ -72,10 +82,25 @@ gbt_bitcmp(const void *a, const void *b, Oid collation, FmgrInfo *flinfo)
 }

+/*
+ * Convert a leaf-page bit/varbit value to internal-page form.
+ *
+ * The important change here is to remove the bit_len field so that
+ * what we have left looks like a bytea.
+ *
+ * The business with padding to INTALIGN length appears entirely historical,
+ * but we can't remove that without breaking on-disk compatibility.
+ * For example, if the lower bound of some leaf page is the 20-bit string
+ * of all ones, with data contents FFFFF0, this code pads it to bytea FFFFF000
+ * in the internal key representing that page.  If, when we search the index
+ * for that value, we did not again pad to FFFFF000, then byteacmp would
+ * say that the query is strictly less than the lower bound so we would not
+ * descend to that leaf page.
+ */
 static bytea *
-gbt_bit_xfrm(bytea *leaf)
+gbt_bit_xfrm(VarBit *leaf)
 {
-	bytea	   *out = leaf;
+	bytea	   *out;
 	int			sz = VARBITBYTES(leaf) + VARHDRSZ;
 	int			padded_sz = INTALIGN(sz);
 
@@ -88,17 +113,19 @@ gbt_bit_xfrm(bytea *leaf)
 	return out;
 }
 
-
-
-
+/*
+ * Convert a GBT_VARKEY representation of a leaf key to a palloc'd
+ * GBT_VARKEY representation of an internal key.
+ * We assume lower == upper since it's a leaf key.
+ */
 static GBT_VARKEY *
 gbt_bit_l2n(GBT_VARKEY *leaf, FmgrInfo *flinfo)
 {
-	GBT_VARKEY *out = leaf;
+	GBT_VARKEY *out;
 	GBT_VARKEY_R r = gbt_var_key_readable(leaf);
 	bytea	   *o;
 
-	o = gbt_bit_xfrm(r.lower);
+	o = gbt_bit_xfrm((VarBit *) r.lower);
 	r.upper = r.lower = o;
 	out = gbt_var_key_copy(&r);
 	pfree(o);
@@ -110,14 +137,14 @@ static const gbtree_vinfo tinfo =
 {
 	gbt_t_bit,
 	0,
-	true,
+	true,						/* internal keys can be truncated */
 	gbt_bitgt,
 	gbt_bitge,
 	gbt_biteq,
 	gbt_bitle,
 	gbt_bitlt,
 	gbt_bitcmp,
-	gbt_bit_l2n
+	gbt_bit_l2n					/* leaf to internal transformation */
 };

@@ -137,7 +164,7 @@ Datum
 gbt_bit_consistent(PG_FUNCTION_ARGS)
 {
 	GISTENTRY  *entry = (GISTENTRY *) PG_GETARG_POINTER(0);
-	void	   *query = DatumGetByteaP(PG_GETARG_DATUM(1));
+	VarBit	   *query = PG_GETARG_VARBIT_P(1);
 	StrategyNumber strategy = (StrategyNumber) PG_GETARG_UINT16(2);
 #ifdef NOT_USED
 	Oid			subtype = PG_GETARG_OID(3);
@@ -155,7 +182,8 @@ gbt_bit_consistent(PG_FUNCTION_ARGS)
 									true, &tinfo, fcinfo->flinfo);
 	else
 	{
-		bytea	   *q = gbt_bit_xfrm((bytea *) query);
+		/* Must convert to internal form to compare to internal-page entries */
+		bytea	   *q = gbt_bit_xfrm(query);
 
 		retval = gbt_var_consistent(&r, q, strategy, PG_GET_COLLATION(),
 									false, &tinfo, fcinfo->flinfo);
@@ -217,6 +245,7 @@ gbt_bit_ssup_cmp(Datum x, Datum y, SortSupport ssup)
 	Datum		result;
 
 	/* for leaf items we expect lower == upper, so only compare lower */
+	/* XXX shouldn't this use bitcmp() ? */
 	result = DirectFunctionCall2(byteacmp,
 								 PointerGetDatum(arg1.lower),
 								 PointerGetDatum(arg2.lower));
diff --git a/contrib/btree_gist/btree_bytea.c b/contrib/btree_gist/btree_bytea.c
index 50bb24308e9..8411598a99b 100644
--- a/contrib/btree_gist/btree_bytea.c
+++ b/contrib/btree_gist/btree_bytea.c
@@ -1,5 +1,7 @@
 /*
  * contrib/btree_gist/btree_bytea.c
+ *
+ * Support for bytea data type.
  */
 #include "postgres.h"
 
@@ -72,7 +74,7 @@ static const gbtree_vinfo tinfo =
 {
 	gbt_t_bytea,
 	0,
-	true,
+	true,						/* internal keys can be truncated */
 	gbt_byteagt,
 	gbt_byteage,
 	gbt_byteaeq,
diff --git a/contrib/btree_gist/btree_numeric.c b/contrib/btree_gist/btree_numeric.c
index dba04c3a1b3..8a506865b17 100644
--- a/contrib/btree_gist/btree_numeric.c
+++ b/contrib/btree_gist/btree_numeric.c
@@ -1,5 +1,7 @@
 /*
  * contrib/btree_gist/btree_numeric.c
+ *
+ * Support for numeric data type.
  */
 #include "postgres.h"
 
@@ -73,11 +75,16 @@ gbt_numeric_cmp(const void *a, const void *b, Oid collation, FmgrInfo *flinfo)
 }

+/*
+ * We could conceivably support internal-key truncation here, but it would
+ * require custom truncation code, and most values wouldn't be long enough
+ * to make it worthwhile.
+ */
 static const gbtree_vinfo tinfo =
 {
 	gbt_t_numeric,
 	0,
-	false,
+	false,						/* no truncation permitted */
 	gbt_numeric_gt,
 	gbt_numeric_ge,
 	gbt_numeric_eq,
diff --git a/contrib/btree_gist/btree_text.c b/contrib/btree_gist/btree_text.c
index 2ac12f1cab2..e7269ba3e7c 100644
--- a/contrib/btree_gist/btree_text.c
+++ b/contrib/btree_gist/btree_text.c
@@ -1,5 +1,7 @@
 /*
  * contrib/btree_gist/btree_text.c
+ *
+ * Support for text and bpchar types.
  */
 #include "postgres.h"
 
@@ -78,11 +80,17 @@ gbt_textcmp(const void *a, const void *b, Oid collation, FmgrInfo *flinfo)
 												 PointerGetDatum(b)));
 }
 
+/*
+ * Originally this module permitted truncation of internal keys, but that
+ * tends to result in wrong comparison answers if the collation is any
+ * more complicated than C.  The prefix-match hack used for simpler types
+ * can't fix it, either.
+ */
 static gbtree_vinfo tinfo =
 {
 	gbt_t_text,
 	0,
-	false,
+	false,						/* no truncation permitted */
 	gbt_textgt,
 	gbt_textge,
 	gbt_texteq,
@@ -152,7 +160,7 @@ static gbtree_vinfo bptinfo =
 {
 	gbt_t_bpchar,
 	0,
-	false,
+	false,						/* as above, no truncation permitted */
 	gbt_bpchargt,
 	gbt_bpcharge,
 	gbt_bpchareq,
diff --git a/contrib/btree_gist/btree_utils_var.c b/contrib/btree_gist/btree_utils_var.c
index 25c3bbe8eac..bceadf5ae60 100644
--- a/contrib/btree_gist/btree_utils_var.c
+++ b/contrib/btree_gist/btree_utils_var.c
@@ -1,5 +1,7 @@
 /*
  * contrib/btree_gist/btree_utils_var.c
+ *
+ * Common routines for btree_gist code working with varlena indexed data types.
  */
 #include "postgres.h"
 
@@ -37,6 +39,7 @@ gbt_var_decompress(PG_FUNCTION_ARGS)
 	GISTENTRY  *entry = (GISTENTRY *) PG_GETARG_POINTER(0);
 	GBT_VARKEY *key = (GBT_VARKEY *) PG_DETOAST_DATUM(entry->key);
 
+	/* We only need a new GISTENTRY if detoasting did something */
 	if (key != (GBT_VARKEY *) DatumGetPointer(entry->key))
 	{
 		GISTENTRY  *retval = palloc_object(GISTENTRY);
@@ -51,7 +54,10 @@ gbt_var_decompress(PG_FUNCTION_ARGS)
 	PG_RETURN_POINTER(entry);
 }
 
-/* Returns a better readable representation of variable key ( sets pointer ) */
+/*
+ * Extract a "readable" representation of a GBT_VARKEY, containing direct
+ * pointers to the contained lower and upper datums.
+ */
 GBT_VARKEY_R
 gbt_var_key_readable(const GBT_VARKEY *k)
 {
@@ -83,7 +89,10 @@ gbt_var_key_from_datum(const varlena *u)
 }
 
 /*
- * Create an entry to store in the index, from lower and upper bound.
+ * Create a key entry to store in the index, from lower and upper bound.
+ *
+ * This code assumes that none of the types we work with require more
+ * than INTALIGN alignment.
  */
 GBT_VARKEY *
 gbt_var_key_copy(const GBT_VARKEY_R *u)
@@ -100,16 +109,17 @@ gbt_var_key_copy(const GBT_VARKEY_R *u)
 	return r;
 }
 
-
+/*
+ * Convert a GBT_VARKEY in leaf form to a GBT_VARKEY in internal form.
+ * No-op if the data type doesn't require a transformation.
+ */
 static GBT_VARKEY *
 gbt_var_leaf2node(GBT_VARKEY *leaf, const gbtree_vinfo *tinfo, FmgrInfo *flinfo)
 {
-	GBT_VARKEY *out = leaf;
-
 	if (tinfo->f_l2n)
-		out = tinfo->f_l2n(leaf, flinfo);
-
-	return out;
+		return tinfo->f_l2n(leaf, flinfo);
+	else
+		return leaf;
 }

@@ -173,7 +183,10 @@ gbt_var_node_cp_len(const GBT_VARKEY *node, const gbtree_vinfo *tinfo)

 /*
- * returns true, if query matches prefix ( common prefix )
+ * returns true if query matches prefix up to the length of the prefix
+ *
+ * We need this to avoid edge-case problems when the "prefix" is a truncated
+ * datum; see discussion in btree_utils_var.h.
  */
 static bool
 gbt_bytea_pf_match(const bytea *pf, const bytea *query, const gbtree_vinfo *tinfo)
@@ -195,7 +208,14 @@ gbt_bytea_pf_match(const bytea *pf, const bytea *query, const gbtree_vinfo *tinf

 /*
- * returns true, if query matches node using common prefix
+ * returns true if query matches node according to common-prefix rule
+ *
+ * If the data type is truncatable, then a shortened upper bound must be
+ * considered to include all values that match it up to its own length,
+ * even though longer values would normally be considered larger.
+ *
+ * XXX isn't the check against node->lower useless?
+ * A truncated lower bound would already be less than all included values.
  */
 static bool
 gbt_var_node_pf_match(const GBT_VARKEY_R *node, const bytea *query, const gbtree_vinfo *tinfo)
@@ -207,13 +227,16 @@ gbt_var_node_pf_match(const GBT_VARKEY_R *node, const bytea *query, const gbtree

 /*
- *  truncates / compresses the node key
- *  cpf_length .. common prefix length
+ * truncates / compresses the node key
+ *
+ * cpf_length is the common prefix length of the lower and upper values.
+ * We truncate to that plus one byte, so that the node represents a range
+ * of leaf values but doesn't have undue specificity.
  */
 static GBT_VARKEY *
 gbt_var_node_truncate(const GBT_VARKEY *node, int32 cpf_length, const gbtree_vinfo *tinfo)
 {
-	GBT_VARKEY *out = NULL;
+	GBT_VARKEY *out;
 	GBT_VARKEY_R r = gbt_var_key_readable(node);
 	int32		len1 = VARSIZE(r.lower) - VARHDRSZ;
 	int32		len2 = VARSIZE(r.upper) - VARHDRSZ;
@@ -246,7 +269,7 @@ gbt_var_bin_union(Datum *u, GBT_VARKEY *e, Oid collation,
 	GBT_VARKEY_R eo = gbt_var_key_readable(e);
 	GBT_VARKEY_R nr;
 
-	if (eo.lower == eo.upper)	/* leaf */
+	if (eo.lower == eo.upper)	/* if leaf, convert to internal form */
 	{
 		GBT_VARKEY *tmp;
 
@@ -355,7 +378,7 @@ gbt_var_union(const GistEntryVector *entryvec, int32 *size, Oid collation,
 	if (tinfo->trnc)
 	{
 		int32		plen;
-		GBT_VARKEY *trc = NULL;
+		GBT_VARKEY *trc;
 
 		plen = gbt_var_node_cp_len((GBT_VARKEY *) DatumGetPointer(out), tinfo);
 		trc = gbt_var_node_truncate((GBT_VARKEY *) DatumGetPointer(out), plen + 1, tinfo);
@@ -396,7 +419,7 @@ gbt_var_penalty(float *res, const GISTENTRY *o, const GISTENTRY *n,
 	*res = 0.0;
 
 	nk = gbt_var_key_readable(newe);
-	if (nk.lower == nk.upper)	/* leaf */
+	if (nk.lower == nk.upper)	/* if leaf, convert to internal form */
 	{
 		GBT_VARKEY *tmp;
 
diff --git a/contrib/btree_gist/btree_utils_var.h b/contrib/btree_gist/btree_utils_var.h
index f28a5d98ae3..c0ac77cb9ec 100644
--- a/contrib/btree_gist/btree_utils_var.h
+++ b/contrib/btree_gist/btree_utils_var.h
@@ -1,5 +1,7 @@
 /*
  * contrib/btree_gist/btree_utils_var.h
+ *
+ * Declarations for btree_gist code working with varlena indexed data types.
  */
 #ifndef __BTREE_UTILS_VAR_H__
 #define __BTREE_UTILS_VAR_H__
@@ -7,10 +9,21 @@
 #include "access/gist.h"
 #include "btree_gist.h"
 
-/* Variable length key */
+/*
+ * An internal index key (also called a node in some places) includes a
+ * lower and an upper bound, both of which are varlena datums, packed into
+ * a wrapper varlena datum.  We also work with leaf keys, which contain a
+ * single varlena datum wrapped in another varlena; this case can be
+ * distinguished by noting that the wrapper isn't long enough to hold a
+ * second datum.  For simplicity we consider the wrapper to be a bytea.
+ */
 typedef bytea GBT_VARKEY;
 
-/* Better readable key */
+/*
+ * To actually work on a key, we use this more readable struct containing
+ * pointers directly to the lower and upper values.  gbt_var_key_readable()
+ * and gbt_var_key_copy() convert between these two representations.
+ */
 typedef struct
 {
 	bytea	   *lower,
@@ -19,6 +32,27 @@ typedef struct
 
 /*
  * type description
+ *
+ * For types that set the "trnc" flag, the representation of keys on internal
+ * pages may be different from that of keys on leaf pages (which match the
+ * data type's normal representation).  The methods f_gt, f_ge, f_eq, f_le,
+ * f_lt expect to work on the normal representation, and therefore can be used
+ * only with leaf-page index entries.  The f_cmp method expects to work on the
+ * representation used on internal pages, so it must not be used with leaf
+ * entries.  The f_l2n method converts the leaf-page representation to the
+ * internal-page representation; if that pointer is NULL, they're the same.
+ *
+ * Currently, btree_utils_var.c effectively assumes that internal-page keys
+ * of "trnc" types are equivalent to bytea in representation and semantics.
+ * The truncation process shortens the lower+upper bounds of a downlink node
+ * to be of length equal to their common prefix's length plus one byte.
+ * This would not work for types with comparison semantics more complex than
+ * bytewise comparison.  Even then, we need a hack to deal with the fact that
+ * shortening the upper bound would normally lead to its being considered less
+ * than the original maximum leaf-page entry.  We handle that by considering
+ * any search key that matches the bound for the bound's full length to be a
+ * potential match, even if it's longer (see gbt_var_node_pf_match and its
+ * callers).
  */
 typedef struct
 {
-- 
2.52.0

From 79d8d82023c951086b484a52e85524c48625536c Mon Sep 17 00:00:00 2001
From: Tom Lane <[email-masked]>
Date: Thu, 2 Jul 2026 17:19:30 -0400
Subject: [PATCH v3 2/5] Fix btree_gist's NotEqual strategy on internal index
 pages.
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

gbt_var_consistent() handled the <> (BtreeGistNotEqual) strategy without
distinguishing leaf from internal pages, unlike every other strategy.
In particular, it tried to apply the datatype-specific f_eq method,
which is completely wrong since internal keys might not have the same
representation as leaf keys.  This led to OOB reads and potentially
crashes, and most likely to wrong query results as well.

On leaf pages we can apply the inverse of what the Equal strategy does.
On internal pages, just descend unconditionally: there's no useful way
to prove that a leaf page can't contain any unequal values.  (Maybe we
could do something in the case where upper and lower bounds are equal,
which is what the previous coding seemed to be thinking about.  But
that case is unlikely, and anyway it won't work for truncated bounds.)

Reported-by: 王跃林 <[email-masked]>
Author: Ayush Tiwari <[email-masked]>
Reviewed-by: Tom Lane <[email-masked]>
Discussion: https://postgr.es/m/[email-masked]
Backpatch-through: 14
---
 contrib/btree_gist/btree_utils_var.c | 16 ++++++++++++++--
 1 file changed, 14 insertions(+), 2 deletions(-)

diff --git a/contrib/btree_gist/btree_utils_var.c b/contrib/btree_gist/btree_utils_var.c
index bceadf5ae60..99919ca3974 100644
--- a/contrib/btree_gist/btree_utils_var.c
+++ b/contrib/btree_gist/btree_utils_var.c
@@ -594,6 +594,13 @@ gbt_var_consistent(GBT_VARKEY_R *key,
 {
 	bool		retval = false;
 
+	/*
+	 * Remember that f_cmp is for internal pages, f_eq etc for leaf pages, and
+	 * on internal pages we need to check gbt_var_node_pf_match too.
+	 *
+	 * The leaf-page tests use swapped operands (e.g., f_gt(query, lower)
+	 * means "lower < query"), which is why they look reversed.
+	 */
 	switch (strategy)
 	{
 		case BTLessEqualStrategyNumber:
@@ -634,8 +641,13 @@ gbt_var_consistent(GBT_VARKEY_R *key,
 					|| gbt_var_node_pf_match(key, query, tinfo);
 			break;
 		case BtreeGistNotEqualStrategyNumber:
-			retval = !(tinfo->f_eq(query, key->lower, collation, flinfo) &&
-					   tinfo->f_eq(query, key->upper, collation, flinfo));
+			if (is_leaf)
+				retval = !(tinfo->f_eq(query, key->lower, collation, flinfo));
+			else
+			{
+				/* we can never disprove existence of a not-equal entry below */
+				retval = true;
+			}
 			break;
 		default:
 			retval = false;
-- 
2.52.0

From 4a2d39891e1f3801ee68647c9b0aa49ad7027b2c Mon Sep 17 00:00:00 2001
From: Tom Lane <[email-masked]>
Date: Thu, 2 Jul 2026 17:27:31 -0400
Subject: [PATCH v3 3/5] Use the proper comparator in gbt_bit_ssup_cmp.

If we're dealing with leaf entries, the function to call is bitcmp
not byteacmp.  Using byteacmp didn't lead to any obvious failure,
but it did result in sorting the entries in a way not matching the
datatype's actual sort order, so that the constructed index would
be much less efficient than one would expect, and in particular
worse than what you got before this code was added in v18.

We might want to recommend that users reindex btree_gist indexes
on bit/varbit.

Author: Tom Lane <[email-masked]>
Discussion: https://postgr.es/m/[email-masked]
Backpatch-through: 18
---
 contrib/btree_gist/btree_bit.c | 3 +--
 1 file changed, 1 insertion(+), 2 deletions(-)

diff --git a/contrib/btree_gist/btree_bit.c b/contrib/btree_gist/btree_bit.c
index dcfe6ed17e4..8d8e9454268 100644
--- a/contrib/btree_gist/btree_bit.c
+++ b/contrib/btree_gist/btree_bit.c
@@ -245,8 +245,7 @@ gbt_bit_ssup_cmp(Datum x, Datum y, SortSupport ssup)
 	Datum		result;
 
 	/* for leaf items we expect lower == upper, so only compare lower */
-	/* XXX shouldn't this use bitcmp() ? */
-	result = DirectFunctionCall2(byteacmp,
+	result = DirectFunctionCall2(bitcmp,
 								 PointerGetDatum(arg1.lower),
 								 PointerGetDatum(arg2.lower));
 
-- 
2.52.0

From 4584b8d96e250d6e3cc854a279df80f654719d1f Mon Sep 17 00:00:00 2001
From: Tom Lane <[email-masked]>
Date: Thu, 2 Jul 2026 17:39:15 -0400
Subject: [PATCH v3 4/5] Remove useless check against lower bound in
 gbt_var_node_pf_match.

Truncating an internal node's upper bound can cause it to compare
less than some values that in fact are included in the represented
leaf page.  So we need a hack to make sure it looks large enough
to include all values that could be on the page.  But there's no
equivalent issue for the lower bound.  The fact that this code did
a fuzzy comparison for the lower bound seems to be the result of
fuzzy thinking.  Or maybe there was a desire to not assume too much
about what the datatype's comparison rule is; but we've already
fully bought into the premise that internal keys compare like bytea.

The comparable check in gbt_var_penalty() may also be useless, but I'm
not quite sure.  In any case that seems negligible from a performance
standpoint, so I left it alone.

Author: Tom Lane <[email-masked]>
Discussion: https://postgr.es/m/[email-masked]
---
 contrib/btree_gist/btree_utils_var.c | 8 +++-----
 1 file changed, 3 insertions(+), 5 deletions(-)

diff --git a/contrib/btree_gist/btree_utils_var.c b/contrib/btree_gist/btree_utils_var.c
index 99919ca3974..f1b13741630 100644
--- a/contrib/btree_gist/btree_utils_var.c
+++ b/contrib/btree_gist/btree_utils_var.c
@@ -213,16 +213,14 @@ gbt_bytea_pf_match(const bytea *pf, const bytea *query, const gbtree_vinfo *tinf
  * If the data type is truncatable, then a shortened upper bound must be
  * considered to include all values that match it up to its own length,
  * even though longer values would normally be considered larger.
- *
- * XXX isn't the check against node->lower useless?
- * A truncated lower bound would already be less than all included values.
+ * We don't need to check the lower bound though: shortening it just
+ * makes it even smaller.
  */
 static bool
 gbt_var_node_pf_match(const GBT_VARKEY_R *node, const bytea *query, const gbtree_vinfo *tinfo)
 {
 	return (tinfo->trnc &&
-			(gbt_bytea_pf_match(node->lower, query, tinfo) ||
-			 gbt_bytea_pf_match(node->upper, query, tinfo)));
+			gbt_bytea_pf_match(node->upper, query, tinfo));
 }

-- 
2.52.0

From 4225a4b2e51757f12d42459cd993dee674c04739 Mon Sep 17 00:00:00 2001
From: Tom Lane <[email-masked]>
Date: Thu, 2 Jul 2026 18:11:10 -0400
Subject: [PATCH v3 5/5] Remove btree_gist's useless logic for encoding-aware
 truncation.

gbt_var_node_cp_len() contained logic to ensure that its choice of
a common prefix length didn't truncate away part of a multibyte
character.  However, that was really dead code, because we have not
allowed truncation of text-string data types since ef770cbb6, and
it seems unlikely that that behavior could ever get resurrected.
The code is still reachable via gbt_var_penalty, but for that
usage it hardly matters if we break in the middle of a multibyte
character: we're just calculating a small correction factor that
is arguably bunkum anyway in non-C locales.

Hence, delete said code.  That actually removes all need for
gbtree_vinfo.eml, which allows const-ification of the gbtree_vinfo
structs in which we were changing it, which removes one headache
for future attempts to thread-ify the backend.

(Curiously, all this infrastructure was itself added by ef770cbb6.
Not sure why Teodor didn't see the contradiction.)

Author: Tom Lane <[email-masked]>
Discussion: https://postgr.es/m/[email-masked]
---
 contrib/btree_gist/btree_bit.c       |  1 -
 contrib/btree_gist/btree_bytea.c     |  1 -
 contrib/btree_gist/btree_numeric.c   |  1 -
 contrib/btree_gist/btree_text.c      | 21 ++--------------
 contrib/btree_gist/btree_utils_var.c | 36 +++++-----------------------
 contrib/btree_gist/btree_utils_var.h |  2 --
 6 files changed, 8 insertions(+), 54 deletions(-)

diff --git a/contrib/btree_gist/btree_bit.c b/contrib/btree_gist/btree_bit.c
index 8d8e9454268..5fb8c8b82f0 100644
--- a/contrib/btree_gist/btree_bit.c
+++ b/contrib/btree_gist/btree_bit.c
@@ -136,7 +136,6 @@ gbt_bit_l2n(GBT_VARKEY *leaf, FmgrInfo *flinfo)
 static const gbtree_vinfo tinfo =
 {
 	gbt_t_bit,
-	0,
 	true,						/* internal keys can be truncated */
 	gbt_bitgt,
 	gbt_bitge,
diff --git a/contrib/btree_gist/btree_bytea.c b/contrib/btree_gist/btree_bytea.c
index 8411598a99b..bcb469e8c86 100644
--- a/contrib/btree_gist/btree_bytea.c
+++ b/contrib/btree_gist/btree_bytea.c
@@ -73,7 +73,6 @@ gbt_byteacmp(const void *a, const void *b, Oid collation, FmgrInfo *flinfo)
 static const gbtree_vinfo tinfo =
 {
 	gbt_t_bytea,
-	0,
 	true,						/* internal keys can be truncated */
 	gbt_byteagt,
 	gbt_byteage,
diff --git a/contrib/btree_gist/btree_numeric.c b/contrib/btree_gist/btree_numeric.c
index 8a506865b17..7a75e2ed00d 100644
--- a/contrib/btree_gist/btree_numeric.c
+++ b/contrib/btree_gist/btree_numeric.c
@@ -83,7 +83,6 @@ gbt_numeric_cmp(const void *a, const void *b, Oid collation, FmgrInfo *flinfo)
 static const gbtree_vinfo tinfo =
 {
 	gbt_t_numeric,
-	0,
 	false,						/* no truncation permitted */
 	gbt_numeric_gt,
 	gbt_numeric_ge,
diff --git a/contrib/btree_gist/btree_text.c b/contrib/btree_gist/btree_text.c
index e7269ba3e7c..a8657fe461a 100644
--- a/contrib/btree_gist/btree_text.c
+++ b/contrib/btree_gist/btree_text.c
@@ -86,10 +86,9 @@ gbt_textcmp(const void *a, const void *b, Oid collation, FmgrInfo *flinfo)
  * more complicated than C.  The prefix-match hack used for simpler types
  * can't fix it, either.
  */
-static gbtree_vinfo tinfo =
+static const gbtree_vinfo tinfo =
 {
 	gbt_t_text,
-	0,
 	false,						/* no truncation permitted */
 	gbt_textgt,
 	gbt_textge,
@@ -156,10 +155,9 @@ gbt_bpcharcmp(const void *a, const void *b, Oid collation, FmgrInfo *flinfo)
 												 PointerGetDatum(b)));
 }
 
-static gbtree_vinfo bptinfo =
+static const gbtree_vinfo bptinfo =
 {
 	gbt_t_bpchar,
-	0,
 	false,						/* as above, no truncation permitted */
 	gbt_bpchargt,
 	gbt_bpcharge,
@@ -180,11 +178,6 @@ gbt_text_compress(PG_FUNCTION_ARGS)
 {
 	GISTENTRY  *entry = (GISTENTRY *) PG_GETARG_POINTER(0);
 
-	if (tinfo.eml == 0)
-	{
-		tinfo.eml = pg_database_encoding_max_length();
-	}
-
 	PG_RETURN_POINTER(gbt_var_compress(entry, &tinfo));
 }
 
@@ -212,11 +205,6 @@ gbt_text_consistent(PG_FUNCTION_ARGS)
 	/* All cases served by this function are exact */
 	*recheck = false;
 
-	if (tinfo.eml == 0)
-	{
-		tinfo.eml = pg_database_encoding_max_length();
-	}
-
 	retval = gbt_var_consistent(&r, query, strategy, PG_GET_COLLATION(),
 								GIST_LEAF(entry), &tinfo, fcinfo->flinfo);
 
@@ -240,11 +228,6 @@ gbt_bpchar_consistent(PG_FUNCTION_ARGS)
 	/* All cases served by this function are exact */
 	*recheck = false;
 
-	if (bptinfo.eml == 0)
-	{
-		bptinfo.eml = pg_database_encoding_max_length();
-	}
-
 	retval = gbt_var_consistent(&r, query, strategy, PG_GET_COLLATION(),
 								GIST_LEAF(entry), &bptinfo, fcinfo->flinfo);
 	PG_RETURN_BOOL(retval);
diff --git a/contrib/btree_gist/btree_utils_var.c b/contrib/btree_gist/btree_utils_var.c
index f1b13741630..60373aaf07c 100644
--- a/contrib/btree_gist/btree_utils_var.c
+++ b/contrib/btree_gist/btree_utils_var.c
@@ -126,56 +126,32 @@ gbt_var_leaf2node(GBT_VARKEY *leaf, const gbtree_vinfo *tinfo, FmgrInfo *flinfo)
 /*
  * returns the common prefix length of a node key
  *
- * If the underlying type is character data, the prefix length may point in
- * the middle of a multibyte character.
+ * This is not used for any cases where the underlying type is character data
+ * (except in gbt_var_penalty, where it doesn't matter since we're just making
+ * an estimated correction not constructing a truncated string).  If it were,
+ * we'd need to be careful not to truncate in the middle of a multibyte
+ * character.
  */
 static int32
 gbt_var_node_cp_len(const GBT_VARKEY *node, const gbtree_vinfo *tinfo)
 {
 	GBT_VARKEY_R r = gbt_var_key_readable(node);
 	int32		i = 0;
-	int32		l_left_to_match = 0;
-	int32		l_total = 0;
 	int32		t1len = VARSIZE(r.lower) - VARHDRSZ;
 	int32		t2len = VARSIZE(r.upper) - VARHDRSZ;
 	int32		ml = Min(t1len, t2len);
 	char	   *p1 = VARDATA(r.lower);
 	char	   *p2 = VARDATA(r.upper);
-	const char *end1 = p1 + t1len;
-	const char *end2 = p2 + t2len;
 
 	if (ml == 0)
 		return 0;
 
 	while (i < ml)
 	{
-		if (tinfo->eml > 1 && l_left_to_match == 0)
-		{
-			l_total = pg_mblen_range(p1, end1);
-			if (l_total != pg_mblen_range(p2, end2))
-			{
-				return i;
-			}
-			l_left_to_match = l_total;
-		}
 		if (*p1 != *p2)
-		{
-			if (tinfo->eml > 1)
-			{
-				int32		l_matched_subset = l_total - l_left_to_match;
-
-				/* end common prefix at final byte of last matching char */
-				return i - l_matched_subset;
-			}
-			else
-			{
-				return i;
-			}
-		}
-
+			return i;
 		p1++;
 		p2++;
-		l_left_to_match--;
 		i++;
 	}
 	return ml;					/* lower == upper */
diff --git a/contrib/btree_gist/btree_utils_var.h b/contrib/btree_gist/btree_utils_var.h
index c0ac77cb9ec..4750fc8361e 100644
--- a/contrib/btree_gist/btree_utils_var.h
+++ b/contrib/btree_gist/btree_utils_var.h
@@ -60,8 +60,6 @@ typedef struct
 	/* Attribs */
 
 	enum gbtree_type t;			/* data type */
-	int32		eml;			/* cached pg_database_encoding_max_length (0:
-								 * undefined) */
 	bool		trnc;			/* truncate (=compress) key */
 
 	/* Methods */
-- 
2.52.0

---

From: Ayush Tiwari <[email-masked]>
Date: Fri, 3 Jul 2026 14:10:44 +0530

Hi,

> turned up an additional bug as well as some other opportunities

Thanks for working on the patch!

> I have the impression that those are way simpler.

0001 LGTM

> we should just flip them all.)

I'd lean towards flipping them for readability, though I'm not sure
it's worth the backpatch churn? I guess we could start a new
thread for master on this.

> leading to what's probably a very inefficient index.

It looks right to me, leaf entries are real bit/varbit values, so bitcmp
is the right comparator there.

> v3-0004 and v3-0005 clean up some things that seem like

One small thing on 0005: once gbt_var_node_cp_len stops using
pg_mblen_range, the "#include "mb/pg_wchar.h"" in btree_utils_var.c
looks like it becomes unused.  Might be worth dropping it too.

> this actually is worth doing.

I think that optimization is correct.  For the non-truncatable types
the internal lower/upper are exact min/max (built via f_cmp in
gbt_var_bin_union with no truncation), so if both equal the query
then everything below does too, and pruning can't drop a match.
Are you planning on including it in the backpatch or to keep the
optimization just part of master, and the bug fix backpatched?

Regards,
Ayush

---

From: Tom Lane <[email-masked]>
Date: Fri, 03 Jul 2026 09:50:30 -0400

> optimization just part of master, and the bug fix backpatched?

I was intending to back-patch only the bug fixes, ie 0002 and 0003.
If we make additional cosmetic changes to gbt_var_consistent(),
I could go either way on whether to include those in the back-patch.

BTW, thinking some more about 0004: isn't it pointless to check
gbt_var_node_pf_match in the gbt_var_consistent cases that
only constrain the lower bound, that is LessEqual and Less?  In

                retval = tinfo->f_cmp(query, key->lower, collation, flinfo) >= 0
                    || gbt_var_node_pf_match(key, query, tinfo);

if query >= key->lower then we must search the node, but if it
isn't then the prefix match against upper can't succeed either.

			regards, tom lane

---

From: Ayush Tiwari <[email-masked]>
Date: Fri, 3 Jul 2026 22:18:13 +0530

Hi,

> I could go either way on whether to include those in the back-patch.

Sounds good.

> isn't then the prefix match against upper can't succeed either.

Hmm, looks like the || gbt_var_node_pf_match(...) clause isn't
needed in the BTLessStrategyNumber and BTLessEqualStrategyNumber
cases.  Those only compare against the lower bound, and if query
sorts before lower then it sorts before upper too, so a prefix
match against the (truncated) upper can never succeed there.
I tried changing it locally and ran the regression tests, all passed.

Regards,
Ayush

---

From: Tom Lane <[email-masked]>
Date: Fri, 03 Jul 2026 13:01:19 -0400

For the archives' sake: I tested the 0003 patch (sort with bitcmp
not byteacmp) with the attached script.  I get this with HEAD:

   ->  Index Only Scan using varbitidx on varbittmp  (cost=0.28..219.00 rows=3584 width=0) (actual time=0.146..1.895 rows=3200.00 loops=1)
         Index Cond: (a > '11111'::bit varying)
         Heap Fetches: 0
         Index Searches: 1
         Buffers: shared hit=1 read=110

and this with the bitcmp patch:

   ->  Index Only Scan using varbitidx on varbittmp  (cost=0.28..267.00 rows=3584 width=0) (actual time=0.081..0.955 rows=3200.00 loops=1)
         Index Cond: (a > '11111'::bit varying)
         Heap Fetches: 0
         Index Searches: 1
         Buffers: shared hit=1 read=47

You might get different hit-vs-read splits depending on your
shared_buffers setting, but the totals should be pretty stable.
So that's a clear win, and indeed running the same script on
v17 shows 50-some buffer accesses, so we did lose performance
from this mistake.

I don't see any win however for a bit(N) column where all the
entries are the same width.  This is unsurprising, since the
bit_len prefix is the same for all and so the bytea sort gives
the same ordering as bitcmp.  Possibly this explains our failure
to notice this problem earlier.

Anyway, 0003 seems quite solid so I'll go push that part.

			regards, tom lane

create extension if not exists btree_gist;

drop table if exists varbittmp;

CREATE TABLE varbittmp (a varbit);

insert into varbittmp
select repeat(i::bit(8)::text, j)::varbit from
  generate_series(0,255) i,
  generate_series(1,100) j;

insert into varbittmp select * from varbittmp;
insert into varbittmp select * from varbittmp;

vacuum analyze varbittmp;

select count(*) from varbittmp;

SELECT count(*) FROM varbittmp WHERE a > '11111';

CREATE INDEX varbitidx ON varbittmp USING GIST ( a );

select pg_size_pretty(pg_relation_size('varbittmp')) as table_size,
       pg_size_pretty(pg_indexes_size('varbittmp')) as index_size,
       pg_indexes_size('varbittmp') / 8192 as index_blocks;

explain (analyze, buffers)
SELECT count(*) FROM varbittmp WHERE a > '11111';

---

From: Tom Lane <[email-masked]>
Date: Fri, 03 Jul 2026 15:36:09 -0400

Ayush Tiwari <[email-masked]> writes:
>> only constrain the lower bound, that is LessEqual and Less?

> I tried changing it locally and ran the regression tests, all passed.

I did some more work on that idea (just cosmetic changes) and
pushed everything.  Thanks for working on this!

			regards, tom lane