Received: from malur.postgresql.org ([217.196.149.56]) by arkaria.postgresql.org with esmtps (TLS1.3) tls TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 (Exim 4.96) (envelope-from ) id 1vvouY-004E7Q-0u for pgsql-hackers@arkaria.postgresql.org; Fri, 27 Feb 2026 03:53:06 +0000 Received: from localhost ([127.0.0.1] helo=malur.postgresql.org) by malur.postgresql.org with esmtp (Exim 4.96) (envelope-from ) id 1vvouW-000FIv-0D for pgsql-hackers@arkaria.postgresql.org; Fri, 27 Feb 2026 03:53:04 +0000 Received: from makus.postgresql.org ([2001:4800:3e1:1::229]) by malur.postgresql.org with esmtps (TLS1.3) tls TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 (Exim 4.96) (envelope-from ) id 1vvouV-000FIe-1f for pgsql-hackers@lists.postgresql.org; Fri, 27 Feb 2026 03:53:03 +0000 Received: from mail-dl1-x1233.google.com ([2607:f8b0:4864:20::1233]) by makus.postgresql.org with esmtps (TLS1.3) tls TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 (Exim 4.98.2) (envelope-from ) id 1vvouR-00000001Px0-371t for pgsql-hackers@lists.postgresql.org; Fri, 27 Feb 2026 03:53:02 +0000 Received: by mail-dl1-x1233.google.com with SMTP id a92af1059eb24-1277e072e2dso1421078c88.0 for ; Thu, 26 Feb 2026 19:53:00 -0800 (PST) ARC-Seal: i=1; a=rsa-sha256; t=1772164380; cv=none; d=google.com; s=arc-20240605; b=biqzeXxT1q7cU0AoaAawfe8fIEn2XXvPJBiVakSLPVGfag2O4pTrZxMpH++wATYvoa LjeddA8j5ekX3vcA7uHml7GU4jnzd+4k3tWo7G5ydkr1LL0YRehiUIuB9PJqxwWHq69z FxnIN8mwjMjYoy4nSK8uP6iBKWDeoRvvAJaaYI4mKox3ffkh9vBAr5n1QF5VZr3e9Zjd b2bWv7YuFZTdPpcykSuaf2P2GqLIXE/Ls008v8N3VMxhGRDfPOouh7c5HNHrz5QmHR9N H1UwQBaN8EepKG16R3sbg9W0MhWQf5LAKFDzslhNZRzCKnRtqcy5hD1QsbQWBhy78wvi DJSg== ARC-Message-Signature: i=1; a=rsa-sha256; c=relaxed/relaxed; d=google.com; s=arc-20240605; h=cc:to:subject:message-id:date:from:in-reply-to:references :mime-version:dkim-signature; bh=E4XBuyqNxhKXd/LPWj7tffdEqdUmf4JFvCqiG1pGogk=; fh=MsS2nBgoUI5cdSwtn00b+U5Zam28lGeOGeSu+lBnwUo=; b=aHhZQQbiTxdIc0GQKQ/ByWidgDt9U6ZOlNnyGfH6oaXJ0hsyuRi1JEInn4/7w49s0o HVb2gynqm259d4/DU6FSxLmLLOo05+03YuMN7rlJjktrRtth71xDFfXsGf9tslj2mma1 D1jMcKKBokJ4N9HtHWetR+qXL6c/G/7LnTj8QxmPHhwShhCo4RW6DsAkT+L4poANy2Kj VZCU24CuC5Em1hc0Ef96QxWc/7qeT5Zfuk80HEhpPfk9dcfxGViGFICgw9fa4lyVc4b6 WwMdUdnn6WWV0gwcHf/fLQzynEJs3tdK5EDmxnk8SfYRflNy6/qFXTbZKym1rvVrHlge W/Vw==; darn=lists.postgresql.org ARC-Authentication-Results: i=1; mx.google.com; arc=none DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=gmail.com; s=20230601; t=1772164380; x=1772769180; darn=lists.postgresql.org; h=cc:to:subject:message-id:date:from:in-reply-to:references :mime-version:from:to:cc:subject:date:message-id:reply-to; bh=E4XBuyqNxhKXd/LPWj7tffdEqdUmf4JFvCqiG1pGogk=; b=XnD3E5EgIxmAXoH/ICs/vcaVv226ZRT0wPvJcZEnVIit5Ozg5GvizCpYh8R8pRo9s8 yozWT2kS3RB26mY/aToPsDGL67Sjy8qGyQJvk2c3t9QsUCF/KDXpcUUQ2puP/OF4wd5r 06cGDQzOZkSrQ7iXOftbCzOUKwFq/lNxMurELovebtDMeL8hZveXDve+gY6TOeeHocL+ WwHQKGg4J5AUGhrY5YcunhoLFuAsRN9DIgk6zIR3n0kB0yDQ0+jmnDWww7UmNKN+FihQ kdXTA1MJSvbrzeXYF0zeqm1ZB+OjrUMfVeE4YrgeytjiVlpFwfwpSVS+/IXRiuIxYgvK HrZQ== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20230601; t=1772164380; x=1772769180; h=cc:to:subject:message-id:date:from:in-reply-to:references :mime-version:x-gm-gg:x-gm-message-state:from:to:cc:subject:date :message-id:reply-to; bh=E4XBuyqNxhKXd/LPWj7tffdEqdUmf4JFvCqiG1pGogk=; b=DpOzXoRVgUYVzlTS2WQEXTapIihNO2X6RE0NCWmr2u5/i+tyn6nuG++YtVYLyY28Qd QjMPtFRUI9TfurcIZ0DZDpfwiDlQmr7YSaFCEM7gxuqaqaCpzmVk3pEG8ekB+ZOU33wZ V7yURGoKysUdOM1yxRNpzgk4/zknsMuTsIxHBsGRHMfNbLXqfL1OWuipXB751CEmwSJE EcaTXXLbi2CXAbvjLIxjni/eJsNmaYCShuYEzH0MfAaNWVjRoT4lfT4DJOlylOBXGprI G3EUiCKBZFjiaiQ8l+Fe6IIUZJdI62qWZ0oYmusLzLFwRnNyPD92qoGBFFBKiVTOsKA8 k35Q== X-Gm-Message-State: AOJu0Yx56VfzWFovFmd+gKSsTexCzEb3rBIgLIkr3J+fx2lXLj/lVYR6 /0PGK4rNemTSx5j1Ojm3scSSxW7Uq6g4qbQJUYScbXZLo7nQPhKKkFojVwW9TQf9vzsx3+Uh9NM 6KKCPAS8w7wNVzlUL4KhaVqTg3s6yz1E= X-Gm-Gg: ATEYQzxfWbVH3rPtwAZO4H7u7Aj/uFmvfbxIhc8Dqg+uByvsYimQHWOitR8EOcs2dTB CJ9oMI1jXmiCeYuDoDUfcQYme8+RuVI5LGJNu7V2rxY6UBshP8O1d5hy89jHj9VndxxtnVlzZKS ZJsaUQzPy/SOj01XEACMX/oW/5A/gaq/I3xfoW80j0uWdjdgk7jwkVpSek+EDonTtNrDC4Ion2y qwb5KaobQa0GXfNkeyE5vPimutyX03ozW19i2QhrnSnHLpwIpT4OXCgWkDeOqTKKraSBQBySNHr 9KVkzqtTZSRiAy2VgLKEoIau6+TE6XNBkyqmCWCzje5mkCBCmI5W62BUpgKaxV/8TkYUsBWOTbA NknWGdAm0GjhkLj7LEXUJ+zeJ X-Received: by 2002:a05:7022:61d:b0:124:9f56:832e with SMTP id a92af1059eb24-127890fc341mr2031412c88.18.1772164379794; Thu, 26 Feb 2026 19:52:59 -0800 (PST) MIME-Version: 1.0 References: In-Reply-To: From: Corey Huinker Date: Thu, 26 Feb 2026 22:52:48 -0500 X-Gm-Features: AaiRm52DNLCQlKneMVHHk_t9oDmHwxfEUOx6mtdgL8DzOpJmFI4iCJ6g4JfMkQA Message-ID: Subject: Re: Defects with invalid stats data for expressions in extended stats To: Michael Paquier Cc: Postgres hackers , Tomas Vondra Content-Type: multipart/alternative; boundary="000000000000576043064bc62c1e" List-Id: List-Help: List-Subscribe: List-Post: List-Owner: List-Archive: Archived-At: Precedence: bulk --000000000000576043064bc62c1e Content-Type: text/plain; charset="UTF-8" Content-Transfer-Encoding: quoted-printable On Thu, Feb 26, 2026 at 7:54=E2=80=AFPM Michael Paquier wrote: > Hi all, > (Adding Tomas in CC., as primary author of the feature dealt with > here, and Corey, due to his work for the restore of extstats.) > > While looking at the proposed patch for the restore of extended > statistics on expressions, I have bumped into two defects that exist > since this feature has been introduced in v15. First, I have thought > that this was a problem only related to the proposed patch, but after > more analysis, I have found that this issue is independent, and can be > triggered without restoring any stats. > > First, one thing that is important to know is that when defining an > extstat object with N expressions, what we finish by storing in the > catalogs is an array of N pg_statistics tuples. Some expressions can > have invalid data, symbolized by this code in extended_stats.c > if (!stats->stats_valid) > { > astate =3D accumArrayResult(astate, > (Datum) 0, > true, > typOid, > CurrentMemoryContext); > continue; > } > > There is nothing wrong with that. Having N elements in the array of > pg_statistics tuples is a requirement, and the code clearly intends > that. There should be no more and no less elements, and this is used > as a marker to let the code that loads this catalog data that nothing > could be computed. This data is inserted when we run ANALYZE. > > Some code paths are unfortunately not water-proof with this NULL-ness > handling, and I have found two of them as fixed by 0001. > > 1) When building statistics, lookup_var_attr_stats() has missed the > fact that computing stats for an expression could lead to invalid > stats being generated. examine_attribute() deals with this case by > returning NULL if either: > 1-1) the typanalyze callback returns false, > 1-2) The number of rows returned is negative. > 1-3) For whatever reason in a custom type implementation, the > compute_stats callback is not set. > lookup_var_attr_stats() has been dealing with the case of invalid > stats for attributes, but it has missed the mark after calling > examine_attribute() for each expression. Note that > examine_attribute() exists in both extended_stats.c and analyze.c, > they are very close in shape, and need to rely on the same assumptions > in terms of what the typanalyze callback can return. > lookup_var_attr_stats() has two callers, both are able to deal with > NULL data (aka invalid stats). A consequence of this issue is a set > of NULL pointer dereferences for MCV, ndistinct and dependencies, as > all these code paths expect something to exist for each expression. > As there is no stats data, each of them would crash. At least one > needs to be specified when creating an extstat object. > > 2) When loading statistics, statext_expressions_load() missed that > some elements in the pg_statistics array could be NULL. This issue > can only be triggered if we have some invalid data stored in the > catalogs. This issue can be triggered on any branches with a > typanalyze callback that returns true, where stats_valid is set to > false when computing the stats (all the in-core callbacks set this > flag to true, nobody in their right mind would do that except me here, > I suspect). The restore of extended stats for expressions makes > this defect more easily reachable by *bypassing* the build, but as the > previous sentence describes it is *not* a mandatory requirement > depending on what a typanalyze callback does. Hence, we have patch it > in all the stable branches anyway. The code allows NULL data to exist > for some expressions, but the load does not cope with it. This is > reflected by fixing two code paths: > 2-1) statext_expressions_load() needs to be able to load NULL data. > 2-2) examine_variable() in selfuncs.c needs to lead with this possible > consequence. > All the callers of examine_variable() have an exit path in selfuncs.c > if there is no stats data available, assuming some defaults, in the > event where statsTuple is NULL (aka invalid). Note that it is > possible to reach this path with a typanalyze that returns true, > meaning that some data is available and that we store NULL data to be > stored, but the compute_stats callback has the idea to set stats_valid > to false. We never set stats_valid to false in any of the in-core > callbacks. > > In order to demonstrate these two bugs, I have implemented a test > module called test_custom_types, as of 0002 (btree operator bloats the > module, it is required for extended stats), that includes a simple > custom type with two buggy typanalyze callbacks (one for the build > defect, and one for the load defect). I think that it would be a good > thing to backpatch this module with the main fix, so as we can cover > these fancy scenarios for all released branches. This could be > enlarged for more cases if we have more defects detected in the future > in this area of the code. This affects versions down to v15. > > In order to finish the business with the restore of extended stats for > this release, these defects have to be addressed first. It does not > impact what has been already committed for the restore of extended > stats, fortunately: we have not touched the expressions yet and the > patch is still floating around in this CF. > > I am registering this item in the final CF, as a bug fix. The > typanalyze requirements may sound it like something worth only fixing > on HEAD, but I don't really see a reason why back-branches should not > be fixed as well. > > So, thoughts or comments? > Great detective work. Patch applies for me, but there seems to be some user-specific stuff in the test, which causes it to fail: diff -U3 /home/corey/src/postgres/src/test/modules/test_custom_types/expected/test_c= ustom_types.out /home/corey/src/postgres/build/testrun/test_custom_types/regress/results/te= st_custom_types.out --- /home/corey/src/postgres/src/test/modules/test_custom_types/expected/test_c= ustom_types.out 2026-02-26 22:34:30.928378989 -0500 +++ /home/corey/src/postgres/build/testrun/test_custom_types/regress/results/te= st_custom_types.out 2026-02-26 22:35:53.005346284 -0500 @@ -127,7 +127,7 @@ tablename | test_table statistics_schemaname | public statistics_name | test_stats -statistics_owner | ioltas +statistics_owner | corey expr | func_int_custom(data) inherited | f null_frac | A nitpick about the test - it uses a plpgsql function when we've been moving such trivial functions to SQL standard function bodies for a while now, and they were introduced back in v14 so there's no backporting concern. Blah blah, eat our own dogfood blah. --000000000000576043064bc62c1e Content-Type: text/html; charset="UTF-8" Content-Transfer-Encoding: quoted-printable


On Thu, Feb 26,= 2026 at 7:54=E2=80=AFPM Michael Paquier <michael@paquier.xyz> wrote:
Hi all,
(Adding Tomas in CC., as primary author of the feature dealt with
here, and Corey, due to his work for the restore of extstats.)

While looking at the proposed patch for the restore of extended
statistics on expressions, I have bumped into two defects that exist
since this feature has been introduced in v15.=C2=A0 First, I have thought<= br> that this was a problem only related to the proposed patch, but after
more analysis, I have found that this issue is independent, and can be
triggered without restoring any stats.

First, one thing that is important to know is that when defining an
extstat object with N expressions, what we finish by storing in the
catalogs is an array of N pg_statistics tuples.=C2=A0 Some expressions can<= br> have invalid data, symbolized by this code in extended_stats.c
if (!stats->stats_valid)
{
=C2=A0 =C2=A0 astate =3D accumArrayResult(astate,
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2= =A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 (Datum) 0,
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2= =A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 true,
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2= =A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 typOid,
=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2= =A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 CurrentMemoryContext);
=C2=A0 =C2=A0 continue;
}

There is nothing wrong with that.=C2=A0 Having N elements in the array of pg_statistics tuples is a requirement, and the code clearly intends
that.=C2=A0 There should be no more and no less elements, and this is used<= br> as a marker to let the code that loads this catalog data that nothing
could be computed.=C2=A0 This data is inserted when we run ANALYZE.

Some code paths are unfortunately not water-proof with this NULL-ness
handling, and I have found two of them as fixed by 0001.

1) When building statistics, lookup_var_attr_stats() has missed the
fact that computing stats for an expression could lead to invalid
stats being generated.=C2=A0 examine_attribute() deals with this case by returning NULL if either:
1-1) the typanalyze callback returns false,
1-2) The number of rows returned is negative.
1-3) For whatever reason in a custom type implementation, the
compute_stats callback is not set.
lookup_var_attr_stats() has been dealing with the case of invalid
stats for attributes, but it has missed the mark after calling
examine_attribute() for each expression.=C2=A0 Note that
examine_attribute() exists in both extended_stats.c and analyze.c,
they are very close in shape, and need to rely on the same assumptions
in terms of what the typanalyze callback can return.
lookup_var_attr_stats() has two callers, both are able to deal with
NULL data (aka invalid stats).=C2=A0 A consequence of this issue is a set of NULL pointer dereferences for MCV, ndistinct and dependencies, as
all these code paths expect something to exist for each expression.
As there is no stats data, each of them would crash.=C2=A0 At least one
needs to be specified when creating an extstat object.

2) When loading statistics, statext_expressions_load() missed that
some elements in the pg_statistics array could be NULL.=C2=A0 This issue can only be triggered if we have some invalid data stored in the
catalogs.=C2=A0 This issue can be triggered on any branches with a
typanalyze callback that returns true, where stats_valid is set to
false when computing the stats (all the in-core callbacks set this
flag to true, nobody in their right mind would do that except me here,
I suspect).=C2=A0 The restore of extended stats for expressions makes
this defect more easily reachable by *bypassing* the build, but as the
previous sentence describes it is *not* a mandatory requirement
depending on what a typanalyze callback does.=C2=A0 Hence, we have patch it=
in all the stable branches anyway.=C2=A0 The code allows NULL data to exist=
for some expressions, but the load does not cope with it.=C2=A0 This is
reflected by fixing two code paths:
2-1) statext_expressions_load() needs to be able to load NULL data.
2-2) examine_variable() in selfuncs.c needs to lead with this possible
consequence.
All the callers of examine_variable() have an exit path in selfuncs.c
if there is no stats data available, assuming some defaults, in the
event where statsTuple is NULL (aka invalid).=C2=A0 Note that it is
possible to reach this path with a typanalyze that returns true,
meaning that some data is available and that we store NULL data to be
stored, but the compute_stats callback has the idea to set stats_valid
to false.=C2=A0 We never set stats_valid to false in any of the in-core
callbacks.

In order to demonstrate these two bugs, I have implemented a test
module called test_custom_types, as of 0002 (btree operator bloats the
module, it is required for extended stats), that includes a simple
custom type with two buggy typanalyze callbacks (one for the build
defect, and one for the load defect).=C2=A0 I think that it would be a good=
thing to backpatch this module with the main fix, so as we can cover
these fancy scenarios for all released branches.=C2=A0 This could be
enlarged for more cases if we have more defects detected in the future
in this area of the code.=C2=A0 This affects versions down to v15.

In order to finish the business with the restore of extended stats for
this release, these defects have to be addressed first.=C2=A0 It does not impact what has been already committed for the restore of extended
stats, fortunately: we have not touched the expressions yet and the
patch is still floating around in this CF.

I am registering this item in the final CF, as a bug fix.=C2=A0 The
typanalyze requirements may sound it like something worth only fixing
on HEAD, but I don't really see a reason why back-branches should not be fixed as well.

So, thoughts or comments?

Great detect= ive work.

Patch applies for me, but there seems to= be some user-specific stuff in the test, which causes it to fail:

<= div style=3D"margin-left:40px">diff -U3 /home/corey/src/postgres/src/test/m= odules/test_custom_types/expected/test_custom_types.out /home/corey/src/pos= tgres/build/testrun/test_custom_types/regress/results/test_custom_types.out=
--- /home/corey/src/postgres/src/test/modules/test_custom_types/expecte= d/test_custom_types.out 2026-02-26 22:34:30.928378989 -0500
+++ /home/co= rey/src/postgres/build/testrun/test_custom_types/regress/results/test_custo= m_types.out 2026-02-26 22:35:53.005346284 -0500
@@ -127,7 +127,7 @@
= =C2=A0tablename =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0| test_tabl= e
=C2=A0statistics_schemaname =C2=A0| public
=C2=A0statistics_name = =C2=A0 =C2=A0 =C2=A0 =C2=A0| test_stats
-statistics_owner =C2=A0 =C2=A0 = =C2=A0 | ioltas
+statistics_owner =C2=A0 =C2=A0 =C2=A0 | corey
=C2=A0= expr =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 | func_= int_custom(data)
=C2=A0inherited =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2= =A0 =C2=A0| f
=C2=A0null_frac =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 = =C2=A0|

A nitpick about the test - it uses a plpgs= ql function when we've been moving such trivial functions to SQL standa= rd function bodies for a while now, and they were introduced back in v14 so= there's no backporting concern. Blah blah, eat our own dogfood blah.
--000000000000576043064bc62c1e--