From a020577d9cc617f33f1f0f2749a6f5d6306eea1c Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tomas@vondra.me>
Date: Wed, 19 Mar 2025 17:24:24 +0100
Subject: [PATCH] Keep decompressed filter in brin_bloom_union

The brin_bloom_union() function combines two BRIN summaries, by merging
one filter into the other. With bloom, we have to decompress the filters
first, but the function failed to update the summary to store the merged
filter. The consequence is the index may be missing some of the data,
and return false negatives.

This issue exists since BRIN bloom indexes were introduced in Postgres
14, but it was non-trivial to hit, as the union function was called only
very rarely, when two sessions happen to summarize a range concurrently.
But it got much easier to hit in 17, as parallel builds use the union
function to merge summaries built by workers.

Not only this corrupts the index (in a logical sense), it also leaks
memory, as the decompressed summaries are kept until the end of the
index build.

Fixed by storing a pointer to the new filter, if it was decompressed,
and freeing the original index. Also free the second filter, if it was
decompressed. Backpatch to 14, where BRIN bloom indexes were introduced.

Reported by Arseniy Mukhin, investigation and fix by me.

Reported-by: Arseniy Mukhin
Discussion: https://postgr.es/m/18855-1cf1c8bcc22150e6%40postgresql.org
---
 src/backend/access/brin/brin_bloom.c | 11 +++++++++++
 1 file changed, 11 insertions(+)

diff --git a/src/backend/access/brin/brin_bloom.c b/src/backend/access/brin/brin_bloom.c
index d160fa7f08f..82b425ce37d 100644
--- a/src/backend/access/brin/brin_bloom.c
+++ b/src/backend/access/brin/brin_bloom.c
@@ -692,6 +692,17 @@ brin_bloom_union(PG_FUNCTION_ARGS)
 	/* update the number of bits set in the filter */
 	filter_a->nbits_set = pg_popcount((const char *) filter_a->data, nbytes);
 
+	/* if we decompressed filter_a, update the summary */
+	if (PointerGetDatum(filter_a) != col_a->bv_values[0])
+	{
+		pfree(DatumGetPointer(col_a->bv_values[0]));
+		col_a->bv_values[0] = PointerGetDatum(filter_a);
+	}
+
+	/* also free filter_b, if it was decompressed */
+	if (PointerGetDatum(filter_b) != col_b->bv_values[0])
+		pfree(filter_b);
+
 	PG_RETURN_VOID();
 }
 
-- 
2.48.1

