From 7bf0c13ffd6500a1c4b7e1e2339ccfa69bcf785a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A1rton=20Boros?= Date: Fri, 26 Jun 2026 11:03:18 +0300 Subject: [PATCH 1/7] feat: enable customer installation of amcheck (PSQL-1327) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit amcheck was in supautils' "may be unsafe" list based on upstream PostgreSQL's general caution around physical data structure exposure in error messages. No Supabase-specific exploit was ever documented. The restriction created a support gap: customers hit corrupted indexes after 15→17 upgrades and could not use bt_index_check() to enumerate and fix individual indexes, forcing full REINDEX DATABASE instead. Move amcheck from the superuser-only list to privileged_extensions so customers can install it. Update nix/tests/prime.sql and prime-superuser.sql to match. Co-Authored-By: Claude Sonnet 4.6 --- ansible/files/postgresql_config/supautils.conf.j2 | 4 ++-- nix/tests/prime-superuser.sql | 9 ++++----- nix/tests/prime.sql | 3 ++- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/ansible/files/postgresql_config/supautils.conf.j2 b/ansible/files/postgresql_config/supautils.conf.j2 index 16ce476d6e..b408a6f2eb 100644 --- a/ansible/files/postgresql_config/supautils.conf.j2 +++ b/ansible/files/postgresql_config/supautils.conf.j2 @@ -2,12 +2,12 @@ supautils.extensions_parameter_overrides = '{"pg_cron":{"schema":"pg_catalog"}}' supautils.policy_grants = '{"postgres":["auth.audit_log_entries","auth.flow_state","auth.identities","auth.instances","auth.mfa_amr_claims","auth.mfa_challenges","auth.mfa_factors","auth.oauth_clients","auth.one_time_tokens","auth.refresh_tokens","auth.saml_providers","auth.saml_relay_states","auth.sessions","auth.sso_domains","auth.sso_providers","auth.users","realtime.messages","realtime.subscription","storage.buckets","storage.buckets_analytics","storage.objects","storage.prefixes","storage.s3_multipart_uploads","storage.s3_multipart_uploads_parts"]}' supautils.drop_trigger_grants = '{"postgres":["auth.audit_log_entries","auth.flow_state","auth.identities","auth.instances","auth.mfa_amr_claims","auth.mfa_challenges","auth.mfa_factors","auth.oauth_clients","auth.one_time_tokens","auth.refresh_tokens","auth.saml_providers","auth.saml_relay_states","auth.sessions","auth.sso_domains","auth.sso_providers","auth.users","realtime.messages","realtime.subscription","storage.buckets","storage.buckets_analytics","storage.objects","storage.prefixes","storage.s3_multipart_uploads","storage.s3_multipart_uploads_parts"]}' # full list: address_standardizer, address_standardizer_data_us, adminpack, amcheck, autoinc, bloom, btree_gin, btree_gist, citext, cube, dblink, dict_int, dict_xsyn, earthdistance, file_fdw, fuzzystrmatch, hstore, http, hypopg, index_advisor, insert_username, intagg, intarray, isn, lo, ltree, moddatetime, old_snapshot, orioledb, pageinspect, pg_buffercache, pg_cron, pg_freespacemap, pg_graphql, pg_hashids, pg_jsonschema, pg_net, pg_prewarm, pg_repack, pg_stat_monitor, pg_stat_statements, pg_surgery, pg_tle, pg_trgm, pg_visibility, pg_walinspect, pgaudit, pgcrypto, pgjwt, pgmq, pgroonga, pgroonga_database, pgrouting, pgrowlocks, pgsodium, pgstattuple, pgtap, plcoffee, pljava, plls, plpgsql, plpgsql_check, plv8, postgis, postgis_raster, postgis_sfcgal, postgis_tiger_geocoder, postgis_topology, postgres_fdw, refint, rum, seg, sslinfo, supabase_vault, supautils, tablefunc, tcn, timescaledb, tsm_system_rows, tsm_system_time, unaccent, uuid-ossp, vector, wrappers, xml2 -# omitted because may be unsafe: adminpack, amcheck, file_fdw, lo, old_snapshot, pageinspect, pg_freespacemap, pg_surgery, pg_visibility +# omitted because may be unsafe: adminpack, file_fdw, lo, old_snapshot, pageinspect, pg_freespacemap, pg_surgery, pg_visibility # omitted because deprecated: intagg, xml2 # omitted because doesn't require superuser: pgmq # omitted because protected: plpgsql # NOTE: keep nix/tests/prime-superuser.sql in sync with the "may be unsafe" + "deprecated" lists above. -supautils.privileged_extensions = 'address_standardizer, address_standardizer_data_us, autoinc, bloom, btree_gin, btree_gist, citext, cube, dblink, dict_int, dict_xsyn, earthdistance, fuzzystrmatch, hstore, http, hypopg, index_advisor, insert_username, intarray, isn, ltree, moddatetime, orioledb, pg_buffercache, pg_cron, pg_graphql, pg_hashids, pg_jsonschema, pg_net, pg_prewarm, pg_repack, pg_stat_monitor, pg_stat_statements, pg_tle, pg_trgm, pg_walinspect, pgaudit, pgcrypto, pgjwt, pgroonga, pgroonga_database, pgrouting, pgrowlocks, pgsodium, pgstattuple, pgtap, plcoffee, pljava, plls, plpgsql_check, plv8, postgis, postgis_raster, postgis_sfcgal, postgis_tiger_geocoder, postgis_topology, postgres_fdw, refint, rum, seg, sslinfo, supabase_vault, supautils, tablefunc, tcn, timescaledb, tsm_system_rows, tsm_system_time, unaccent, uuid-ossp, vector, wrappers' +supautils.privileged_extensions = 'address_standardizer, address_standardizer_data_us, amcheck, autoinc, bloom, btree_gin, btree_gist, citext, cube, dblink, dict_int, dict_xsyn, earthdistance, fuzzystrmatch, hstore, http, hypopg, index_advisor, insert_username, intarray, isn, ltree, moddatetime, orioledb, pg_buffercache, pg_cron, pg_graphql, pg_hashids, pg_jsonschema, pg_net, pg_prewarm, pg_repack, pg_stat_monitor, pg_stat_statements, pg_tle, pg_trgm, pg_walinspect, pgaudit, pgcrypto, pgjwt, pgroonga, pgroonga_database, pgrouting, pgrowlocks, pgsodium, pgstattuple, pgtap, plcoffee, pljava, plls, plpgsql_check, plv8, postgis, postgis_raster, postgis_sfcgal, postgis_tiger_geocoder, postgis_topology, postgres_fdw, refint, rum, seg, sslinfo, supabase_vault, supautils, tablefunc, tcn, timescaledb, tsm_system_rows, tsm_system_time, unaccent, uuid-ossp, vector, wrappers' supautils.extension_custom_scripts_path = '/etc/postgresql-custom/extension-custom-scripts' supautils.restrict_extension_versions = 'warn' supautils.privileged_extensions_superuser = 'supabase_admin' diff --git a/nix/tests/prime-superuser.sql b/nix/tests/prime-superuser.sql index 3d3a2c8b77..e8382c0046 100644 --- a/nix/tests/prime-superuser.sql +++ b/nix/tests/prime-superuser.sql @@ -11,10 +11,10 @@ -- ansible/files/postgresql_config: -- -- 1. "omitted because may be unsafe" — supautils.conf.j2. --- Covers the entries available in BOTH PG 15 and PG 17 builds. Two more --- entries from the same list, `adminpack` and `old_snapshot`, were --- removed from contrib in PG 17 and are loaded directly by --- nix/tests/sql/z_15_ext_interface.sql for the PG 15 path. +-- Covers the entries available in BOTH PG 15 and PG 17 builds. Three more +-- entries from the same list, `adminpack`, `amcheck`, and `old_snapshot`, were +-- removed from contrib in PG 17 or enabled for customers, and are loaded directly by +-- nix/tests/sql/z_15_ext_interface.sql for the PG 15 path (adminpack, old_snapshot only). -- -- 2. "omitted because deprecated" — supautils.conf.j2. -- Not in privileged_extensions, so non-superuser can't install. @@ -30,7 +30,6 @@ set client_min_messages = warning; -- Category 1: "may be unsafe" per supautils.conf.j2 -create extension if not exists amcheck; create extension if not exists file_fdw; create extension if not exists lo; create extension if not exists pageinspect; diff --git a/nix/tests/prime.sql b/nix/tests/prime.sql index 24fd8f2eaa..9260e759ae 100644 --- a/nix/tests/prime.sql +++ b/nix/tests/prime.sql @@ -9,12 +9,13 @@ set client_min_messages = warning; -- engines-with-smoke against hosted projects). They live in -- prime-superuser.sql instead — that covers: -- - the "may be unsafe" list from supautils.conf.j2 --- (amcheck, file_fdw, lo, pageinspect, pg_freespacemap, pg_surgery, pg_visibility) +-- (file_fdw, lo, pageinspect, pg_freespacemap, pg_surgery, pg_visibility) -- - the "deprecated" list from supautils.conf.j2 (intagg, xml2) -- Superuser-context harnesses (nix/checks.nix, docker-image-test, -- migrate-tool) source prime-superuser.sql in addition to this file. create extension if not exists address_standardizer; create extension if not exists address_standardizer_data_us; +create extension if not exists amcheck; create extension if not exists autoinc; create extension if not exists bloom; create extension if not exists btree_gin; From a99c408a2c5d49b06e3a0ec8fe6168f16fb877fd Mon Sep 17 00:00:00 2001 From: Sam Rose Date: Tue, 18 Aug 2026 17:00:19 -0400 Subject: [PATCH 2/7] feat: enable customer installation of amcheck (PSQL-1327) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Move amcheck from supautils' superuser-only list into privileged_extensions, pinned to the `extensions` schema. The pin is required. In `public`, ALTER DEFAULT PRIVILEGES grants EXECUTE to anon/authenticated/service_role, and amcheck does no permission check of its own — so unauthenticated PostgREST callers could run bt_index_parent_check() and verify_heapam() against any table. In `extensions` the equivalent rule grants to postgres only --- .../files/postgresql_config/supautils.conf.j2 | 10 +- nix/checks.nix | 1 + nix/ext/tests/amcheck-upgrade.nix | 199 ++++++++++++++++++ nix/tests/expected/amcheck.out | 119 +++++++++++ nix/tests/expected/extensions_schema.out | 3 +- nix/tests/expected/z_15_ext_interface.out | 12 +- nix/tests/expected/z_17_ext_interface.out | 16 +- .../expected/z_multigres-17_ext_interface.out | 16 +- .../expected/z_orioledb-17_ext_interface.out | 16 +- .../z_orioledb-17_extensions_schema.out | 3 +- nix/tests/prime-superuser.sql | 8 +- nix/tests/sql/amcheck.sql | 92 ++++++++ 12 files changed, 458 insertions(+), 37 deletions(-) create mode 100644 nix/ext/tests/amcheck-upgrade.nix create mode 100644 nix/tests/expected/amcheck.out create mode 100644 nix/tests/sql/amcheck.sql diff --git a/ansible/files/postgresql_config/supautils.conf.j2 b/ansible/files/postgresql_config/supautils.conf.j2 index b408a6f2eb..ccd30bc999 100644 --- a/ansible/files/postgresql_config/supautils.conf.j2 +++ b/ansible/files/postgresql_config/supautils.conf.j2 @@ -1,4 +1,12 @@ -supautils.extensions_parameter_overrides = '{"pg_cron":{"schema":"pg_catalog"}}' +# amcheck must stay pinned to `extensions`. supautils creates privileged extensions as +# supabase_admin, whose search_path starts with `public`, and the ALTER DEFAULT PRIVILEGES in +# migrations/db/init-scripts/00000000000000-initial-schema.sql grants EXECUTE on anything +# supabase_admin creates there to anon, authenticated and service_role — i.e. to unauthenticated +# PostgREST callers. amcheck performs no permission check of its own (upstream verify_nbtree.c: +# "Intentionally not checking permissions"), so its REVOKE ALL FROM PUBLIC is the only gate and +# whoever holds EXECUTE can check any index in the database, including auth's. In `extensions` +# the equivalent rule grants to `postgres` only. See PSQL-1327 and nix/tests/sql/amcheck.sql. +supautils.extensions_parameter_overrides = '{"pg_cron":{"schema":"pg_catalog"},"amcheck":{"schema":"extensions"}}' supautils.policy_grants = '{"postgres":["auth.audit_log_entries","auth.flow_state","auth.identities","auth.instances","auth.mfa_amr_claims","auth.mfa_challenges","auth.mfa_factors","auth.oauth_clients","auth.one_time_tokens","auth.refresh_tokens","auth.saml_providers","auth.saml_relay_states","auth.sessions","auth.sso_domains","auth.sso_providers","auth.users","realtime.messages","realtime.subscription","storage.buckets","storage.buckets_analytics","storage.objects","storage.prefixes","storage.s3_multipart_uploads","storage.s3_multipart_uploads_parts"]}' supautils.drop_trigger_grants = '{"postgres":["auth.audit_log_entries","auth.flow_state","auth.identities","auth.instances","auth.mfa_amr_claims","auth.mfa_challenges","auth.mfa_factors","auth.oauth_clients","auth.one_time_tokens","auth.refresh_tokens","auth.saml_providers","auth.saml_relay_states","auth.sessions","auth.sso_domains","auth.sso_providers","auth.users","realtime.messages","realtime.subscription","storage.buckets","storage.buckets_analytics","storage.objects","storage.prefixes","storage.s3_multipart_uploads","storage.s3_multipart_uploads_parts"]}' # full list: address_standardizer, address_standardizer_data_us, adminpack, amcheck, autoinc, bloom, btree_gin, btree_gist, citext, cube, dblink, dict_int, dict_xsyn, earthdistance, file_fdw, fuzzystrmatch, hstore, http, hypopg, index_advisor, insert_username, intagg, intarray, isn, lo, ltree, moddatetime, old_snapshot, orioledb, pageinspect, pg_buffercache, pg_cron, pg_freespacemap, pg_graphql, pg_hashids, pg_jsonschema, pg_net, pg_prewarm, pg_repack, pg_stat_monitor, pg_stat_statements, pg_surgery, pg_tle, pg_trgm, pg_visibility, pg_walinspect, pgaudit, pgcrypto, pgjwt, pgmq, pgroonga, pgroonga_database, pgrouting, pgrowlocks, pgsodium, pgstattuple, pgtap, plcoffee, pljava, plls, plpgsql, plpgsql_check, plv8, postgis, postgis_raster, postgis_sfcgal, postgis_tiger_geocoder, postgis_topology, postgres_fdw, refint, rum, seg, sslinfo, supabase_vault, supautils, tablefunc, tcn, timescaledb, tsm_system_rows, tsm_system_time, unaccent, uuid-ossp, vector, wrappers, xml2 diff --git a/nix/checks.nix b/nix/checks.nix index ab7a98825f..56d55003eb 100644 --- a/nix/checks.nix +++ b/nix/checks.nix @@ -255,6 +255,7 @@ "pg_net_worker_privileges" # needs the authenticated/postgres roles from the full migrations, not present in the CLI prime file "pg_cron_trigger_privileges" # needs pg_cron + the postgres role and cron-schema grants from the full migrations, not in the CLI prime file "supautils_restrict_versions" # needs the postgres role + primed hstore from the full migrations/prime, not present in the CLI variant + "amcheck" # needs the postgres/anon/authenticated/service_role roles and the default privileges from the full migrations, plus amcheck primed by prime.sql # Version-specific extension tests "z_17_ext_interface" "z_17_pg_stat_monitor" diff --git a/nix/ext/tests/amcheck-upgrade.nix b/nix/ext/tests/amcheck-upgrade.nix new file mode 100644 index 0000000000..95a6ea8359 --- /dev/null +++ b/nix/ext/tests/amcheck-upgrade.nix @@ -0,0 +1,199 @@ +# amcheck across a 15 -> 17 pg_upgrade (PSQL-1327). +# +# amcheck is a customer-installable privileged extension pinned to the +# `extensions` schema by supautils.extensions_parameter_overrides. That pin is +# what keeps EXECUTE away from anon/authenticated/service_role: in `public` the +# ALTER DEFAULT PRIVILEGES in initial-schema.sql would grant to all four roles, +# and amcheck performs no permission check of its own, so a grant is a licence +# to check any index in the database. +# +# nix/tests/sql/amcheck.sql asserts that on a fresh install. This test covers +# the part pg_regress cannot reach: whether the schema and the grants survive a +# major-version upgrade. That matters because extension-member function ACLs +# ride on pg_init_privs through pg_upgrade's dump/restore, and because 15 ships +# amcheck 1.3 while 17 ships 1.4 -- so the upgrade also runs an ALTER EXTENSION +# UPDATE that creates new functions, each of which picks up whatever default +# privileges apply at that moment. +# +# Upgraded projects are exactly the population the ticket is about (customers +# hitting corrupt indexes after 15 -> 17), so a silent re-grant here would undo +# the fix for everyone who matters. +# +# Deliberately 15 -> 17 only. There is no upgrade path into orioledb (its +# specialisation wipes the data directory), so an orioledb leg would assert +# nothing about upgrade behaviour and only add VM runtime. +{ self, pkgs }: +let + testLib = import ./lib.nix { inherit self pkgs; }; +in +pkgs.testers.runNixOSTest { + name = "amcheck-upgrade"; + nodes.server = + { ... }: + { + imports = [ + (testLib.makeSupabaseTestConfig { + majorVersion = "15"; + }) + ]; + + specialisation.postgresql17.configuration = testLib.makeUpgradeSpecialisation { + fromMajorVersion = "15"; + toMajorVersion = "17"; + }; + }; + testScript = + { nodes, ... }: + let + pg17-configuration = "${nodes.server.system.build.toplevel}/specialisation/postgresql17"; + in + '' + pg17_configuration = "${pg17-configuration}" + + # postgres holds EXECUTE (with grant option, via the extensions-schema + # default privileges); the three PostgREST roles hold nothing. Aggregated + # over every function the extension owns rather than named signatures, so + # this stays correct as amcheck grows from 6 functions on 15 to 8 on 17. + EXPECTED_ACL = "anon,f,f\nauthenticated,f,f\npostgres,t,t\nservice_role,f,f" + + ACL_QUERY = ( + "select r.rolname, " + "bool_and(has_function_privilege(r.rolname, p.oid, 'execute')), " + "bool_or(has_function_privilege(r.rolname, p.oid, 'execute')) " + "from pg_proc p " + "join pg_depend d on d.objid = p.oid and d.deptype = 'e' " + "join pg_extension e on e.oid = d.refobjid and e.extname = 'amcheck' " + "cross join (values ('postgres'), ('anon'), ('authenticated'), ('service_role')) " + "as r(rolname) group by r.rolname order by r.rolname" + ) + + IDENTITY_QUERY = ( + "select extversion, extnamespace::regnamespace " + "from pg_extension where extname = 'amcheck'" + ) + + def sql(query, role=None): + # Connect as supabase_admin (the bootstrap superuser this harness + # authenticates as) and pick up the target role via the startup packet, + # so each statement runs under its own session rather than being bundled + # into one implicit transaction by a multi-statement -c. + prefix = "" + if role is not None: + prefix = "PGOPTIONS=\"-c role=" + role + "\" " + return server.succeed( + prefix + + "psql -U supabase_admin -d postgres -t -A -F, -v ON_ERROR_STOP=1 -c \"" + + query + + "\"" + ).strip() + + def sql_fails(query, role=None): + prefix = "" + if role is not None: + prefix = "PGOPTIONS=\"-c role=" + role + "\" " + return server.fail( + prefix + + "psql -U supabase_admin -d postgres -t -A -v ON_ERROR_STOP=1 -c \"" + + query + + "\" 2>&1" + ) + + start_all() + + # Wait for full Supabase initialization (postgres + init-scripts + migrations), + # since the roles and default privileges under test come from the migrations. + server.wait_for_unit("supabase-db-init.service") + + with subtest("Preconditions"): + assert sql("select rolsuper from pg_roles where rolname = 'postgres'") == "f", ( + "postgres is a superuser; every grant assertion below would be vacuous" + ) + # Fail loudly if the role-switching mechanism itself is broken, rather + # than silently running the whole suite as supabase_admin. + whoami = sql("select current_user", role="postgres") + assert whoami == "postgres", f"expected to be acting as postgres, got: {whoami}" + + with subtest("A non-superuser installs amcheck on PG 15, pinned to extensions"): + sql("create extension amcheck", role="postgres") + before = sql(IDENTITY_QUERY) + assert before.endswith(",extensions"), ( + f"expected amcheck in the extensions schema, got: {before}" + ) + print(f"PG 15 amcheck: {before}") + + with subtest("PG 15 grants reach postgres only"): + acl_before = sql(ACL_QUERY) + assert acl_before == EXPECTED_ACL, f"unexpected PG 15 grants:\n{acl_before}" + + with subtest("switch to postgresql 17"): + server.execute(f"{pg17_configuration}/bin/switch-to-configuration test >&2") + server.wait_for_unit("postgresql.service") + + with subtest("pg_upgrade preserves amcheck's version, schema and grants"): + after = sql(IDENTITY_QUERY) + assert after == before, f"amcheck changed across pg_upgrade: {before} -> {after}" + acl_after = sql(ACL_QUERY) + assert acl_after == EXPECTED_ACL, f"grants changed across pg_upgrade:\n{acl_after}" + + with subtest("The 1.3 -> 1.4 extension update preserves the grants"): + # The platform runs pg_upgrade's generated update_extensions.sql after + # the upgrade (admin_api_scripts/pg_upgrade_scripts/complete.sh). This + # harness runs raw pg_upgrade, so replay that file when it is present + # and fall back to an explicit ALTER when it is not -- either route + # creates the new 1.4 functions, which is what we need to inspect. + has_script = server.succeed( + "test -f /var/lib/postgresql/update_extensions.sql && echo yes || echo no" + ).strip() + if has_script == "yes": + server.succeed( + "psql -U supabase_admin -d postgres -v ON_ERROR_STOP=1 " + "-f /var/lib/postgresql/update_extensions.sql" + ) + else: + sql("alter extension amcheck update") + + default_version = sql( + "select default_version from pg_available_extensions where name = 'amcheck'" + ) + updated = sql(IDENTITY_QUERY) + installed_version = updated.split(",")[0] + assert installed_version == default_version, ( + f"expected amcheck at PG 17's default {default_version}, got {installed_version}" + ) + # Guard against this subtest quietly becoming a no-op if 15 and 17 ever + # ship the same amcheck version -- there would then be no new functions + # to acquire grants, and nothing here would be under test. + assert installed_version != before.split(",")[0], ( + f"amcheck was already at {installed_version} on PG 15; this subtest no " + "longer exercises a version bump and needs rewriting" + ) + assert updated.endswith(",extensions"), ( + f"amcheck left the extensions schema during the update: {updated}" + ) + + acl_updated = sql(ACL_QUERY) + assert acl_updated == EXPECTED_ACL, ( + f"grants changed when amcheck was updated to {installed_version}:\n{acl_updated}" + ) + + with subtest("The support workflow still works after the upgrade"): + sql( + "create table amcheck_heap(i int primary key) using heap; " + "insert into amcheck_heap select generate_series(1, 100)", + role="postgres", + ) + sql("select extensions.bt_index_check('amcheck_heap_pkey'::regclass)", role="postgres") + # amcheck has no per-relation gate, which is the point: a customer can + # check an auth index they do not own after an upgrade corrupts it. + sql("select extensions.bt_index_check('auth.users_pkey'::regclass)", role="postgres") + + with subtest("The API roles are still locked out after the upgrade"): + for role in ["anon", "authenticated", "service_role"]: + err = sql_fails( + "select extensions.bt_index_check('auth.users_pkey'::regclass)", role=role + ) + assert "permission denied for function bt_index_check" in err, ( + f"expected {role} to be denied, got: {err}" + ) + ''; +} diff --git a/nix/tests/expected/amcheck.out b/nix/tests/expected/amcheck.out new file mode 100644 index 0000000000..370fd27a6f --- /dev/null +++ b/nix/tests/expected/amcheck.out @@ -0,0 +1,119 @@ +-- amcheck is installable by customers (PSQL-1327): it lives in +-- supautils.privileged_extensions, so the non-superuser `postgres` role can +-- `create extension amcheck` and call bt_index_check() to find corrupt indexes +-- without a full REINDEX DATABASE. +-- +-- The schema it lands in is `extensions`. amcheck deliberately +-- performs no permission check of its own -- upstream verify_nbtree.c says +-- "Intentionally not checking permissions" -- so its `REVOKE ALL ... FROM +-- PUBLIC` is the only access control, and any role holding EXECUTE can check +-- any index in the database. +-- +-- Two standing ALTER DEFAULT PRIVILEGES rules decide which role gets that EXECUTE, +-- because supautils creates privileged extensions as supabase_admin: +-- +-- * schema public -> postgres, anon, authenticated, service_role +-- (migrations/db/init-scripts/00000000000000-initial-schema.sql) +-- * schema extensions -> postgres only +-- (migrations/db/migrations/20230224042246_grant_extensions_perms_for_postgres.sql) +-- +-- Landing in public would therefore expose bt_index_parent_check() (ShareLock, +-- blocks writes) and verify_heapam() to unauthenticated PostgREST callers. So +-- supautils.extensions_parameter_overrides pins amcheck to `extensions`, and +-- this suite asserts both the placement and the resulting privilege matrix. +-- +-- Runs against the rendered supautils.conf.j2 with the real migrations applied, +-- so the roles below are the actual platform roles. +-- the platform config pins amcheck's schema +show supautils.extensions_parameter_overrides; + supautils.extensions_parameter_overrides +----------------------------------------------------------------------- + {"pg_cron":{"schema":"pg_catalog"},"amcheck":{"schema":"extensions"}} +(1 row) + +-- precondition: postgres is not a superuser, else every assertion is vacuous +select rolsuper from pg_roles where rolname = 'postgres'; + rolsuper +---------- + f +(1 row) + +-- prime.sql already created amcheck; drop it so the creates below are observable +drop extension if exists amcheck; +-- a non-superuser can install it, and it lands in `extensions` rather than the +-- session search_path's first entry (public) +set role postgres; +create extension amcheck; +reset role; +select extowner::regrole as owner, extnamespace::regnamespace as schema + from pg_extension + where extname = 'amcheck'; + owner | schema +----------------+------------ + supabase_admin | extensions +(1 row) + +-- the override wins over an explicitly requested schema, so the placement +-- cannot be opted out of +drop extension amcheck; +set role postgres; +create extension amcheck with schema public; +reset role; +select extnamespace::regnamespace as schema_after_requesting_public + from pg_extension + where extname = 'amcheck'; + schema_after_requesting_public +-------------------------------- + extensions +(1 row) + +-- the privilege matrix that placement produces. Aggregated over every function +-- the extension owns rather than named signatures, so this stays correct as +-- amcheck gains functions across PG versions (15 ships 6, 17 ships 8) and +-- catches any future addition that arrives with different grants. +select r.rolname, + bool_and(has_function_privilege(r.rolname, p.oid, 'execute')) as all_functions, + bool_or(has_function_privilege(r.rolname, p.oid, 'execute')) as any_function + from pg_proc p + join pg_depend d on d.objid = p.oid and d.deptype = 'e' + join pg_extension e on e.oid = d.refobjid and e.extname = 'amcheck' + cross join (values ('postgres'), ('anon'), ('authenticated'), ('service_role')) as r(rolname) + group by r.rolname + order by r.rolname; + rolname | all_functions | any_function +---------------+---------------+-------------- + anon | f | f + authenticated | f | f + postgres | t | t + service_role | f | f +(4 rows) + +-- the support use case: postgres checks its own index, and -- since amcheck has +-- no per-relation gate -- one on a table it does not own, which is what makes +-- the extension useful after a 15->17 upgrade corrupts an auth index +set role postgres; +create table amcheck_heap(i int primary key) using heap; +insert into amcheck_heap select generate_series(1, 100); +select extensions.bt_index_check('amcheck_heap_pkey'::regclass); + bt_index_check +---------------- + +(1 row) + +select extensions.bt_index_check('auth.users_pkey'::regclass); + bt_index_check +---------------- + +(1 row) + +reset role; +-- the API roles must not reach it. The denial is at the function level, so it +-- applies to every target relation, not just this one. +set role anon; +select extensions.bt_index_check('auth.users_pkey'::regclass); +ERROR: permission denied for function bt_index_check +reset role; +-- restore the state prime.sql created (amcheck present, in `extensions`) +set role postgres; +drop table amcheck_heap; +reset role; diff --git a/nix/tests/expected/extensions_schema.out b/nix/tests/expected/extensions_schema.out index 7d3dffa95f..c2fca7f222 100644 --- a/nix/tests/expected/extensions_schema.out +++ b/nix/tests/expected/extensions_schema.out @@ -15,8 +15,9 @@ order by e.extname; extension_name | schema_name | extension_owner --------------------+-------------+----------------- + amcheck | extensions | supabase_admin pg_stat_statements | extensions | supabase_admin pgcrypto | extensions | supabase_admin uuid-ossp | extensions | supabase_admin -(3 rows) +(4 rows) diff --git a/nix/tests/expected/z_15_ext_interface.out b/nix/tests/expected/z_15_ext_interface.out index 622080a4fa..94de191ef4 100644 --- a/nix/tests/expected/z_15_ext_interface.out +++ b/nix/tests/expected/z_15_ext_interface.out @@ -181,12 +181,12 @@ order by adminpack | pg_catalog | pg_file_unlink | text | boolean adminpack | pg_catalog | pg_file_write | text, text, boolean | bigint adminpack | pg_catalog | pg_logdir_ls | | SETOF record - amcheck | public | bt_index_check | index regclass | void - amcheck | public | bt_index_check | index regclass, heapallindexed boolean | void - amcheck | public | bt_index_parent_check | index regclass | void - amcheck | public | bt_index_parent_check | index regclass, heapallindexed boolean | void - amcheck | public | bt_index_parent_check | index regclass, heapallindexed boolean, rootdescend boolean | void - amcheck | public | verify_heapam | relation regclass, on_error_stop boolean, check_toast boolean, skip text, startblock bigint, endblock bigint, OUT blkno bigint, OUT offnum integer, OUT attnum integer, OUT msg text | SETOF record + amcheck | extensions | bt_index_check | index regclass | void + amcheck | extensions | bt_index_check | index regclass, heapallindexed boolean | void + amcheck | extensions | bt_index_parent_check | index regclass | void + amcheck | extensions | bt_index_parent_check | index regclass, heapallindexed boolean | void + amcheck | extensions | bt_index_parent_check | index regclass, heapallindexed boolean, rootdescend boolean | void + amcheck | extensions | verify_heapam | relation regclass, on_error_stop boolean, check_toast boolean, skip text, startblock bigint, endblock bigint, OUT blkno bigint, OUT offnum integer, OUT attnum integer, OUT msg text | SETOF record autoinc | public | autoinc | | trigger bloom | public | blhandler | internal | index_am_handler btree_gin | public | gin_btree_consistent | internal, smallint, anyelement, integer, internal, internal | boolean diff --git a/nix/tests/expected/z_17_ext_interface.out b/nix/tests/expected/z_17_ext_interface.out index 6625cd8bc4..22adb257c2 100644 --- a/nix/tests/expected/z_17_ext_interface.out +++ b/nix/tests/expected/z_17_ext_interface.out @@ -162,14 +162,14 @@ order by address_standardizer | public | parse_address | text, OUT num text, OUT street text, OUT street2 text, OUT address1 text, OUT city text, OUT state text, OUT zip text, OUT zipplus text, OUT country text | record address_standardizer | public | standardize_address | lextab text, gaztab text, rultab text, address text | stdaddr address_standardizer | public | standardize_address | lextab text, gaztab text, rultab text, micro text, macro text | stdaddr - amcheck | public | bt_index_check | index regclass | void - amcheck | public | bt_index_check | index regclass, heapallindexed boolean, checkunique boolean | void - amcheck | public | bt_index_check | index regclass, heapallindexed boolean | void - amcheck | public | bt_index_parent_check | index regclass | void - amcheck | public | bt_index_parent_check | index regclass, heapallindexed boolean, rootdescend boolean, checkunique boolean | void - amcheck | public | bt_index_parent_check | index regclass, heapallindexed boolean | void - amcheck | public | bt_index_parent_check | index regclass, heapallindexed boolean, rootdescend boolean | void - amcheck | public | verify_heapam | relation regclass, on_error_stop boolean, check_toast boolean, skip text, startblock bigint, endblock bigint, OUT blkno bigint, OUT offnum integer, OUT attnum integer, OUT msg text | SETOF record + amcheck | extensions | bt_index_check | index regclass | void + amcheck | extensions | bt_index_check | index regclass, heapallindexed boolean, checkunique boolean | void + amcheck | extensions | bt_index_check | index regclass, heapallindexed boolean | void + amcheck | extensions | bt_index_parent_check | index regclass | void + amcheck | extensions | bt_index_parent_check | index regclass, heapallindexed boolean, rootdescend boolean, checkunique boolean | void + amcheck | extensions | bt_index_parent_check | index regclass, heapallindexed boolean | void + amcheck | extensions | bt_index_parent_check | index regclass, heapallindexed boolean, rootdescend boolean | void + amcheck | extensions | verify_heapam | relation regclass, on_error_stop boolean, check_toast boolean, skip text, startblock bigint, endblock bigint, OUT blkno bigint, OUT offnum integer, OUT attnum integer, OUT msg text | SETOF record autoinc | public | autoinc | | trigger bloom | public | blhandler | internal | index_am_handler btree_gin | public | gin_btree_consistent | internal, smallint, anyelement, integer, internal, internal | boolean diff --git a/nix/tests/expected/z_multigres-17_ext_interface.out b/nix/tests/expected/z_multigres-17_ext_interface.out index c66b18d7a9..76a6675730 100644 --- a/nix/tests/expected/z_multigres-17_ext_interface.out +++ b/nix/tests/expected/z_multigres-17_ext_interface.out @@ -162,14 +162,14 @@ order by address_standardizer | public | parse_address | text, OUT num text, OUT street text, OUT street2 text, OUT address1 text, OUT city text, OUT state text, OUT zip text, OUT zipplus text, OUT country text | record address_standardizer | public | standardize_address | lextab text, gaztab text, rultab text, address text | stdaddr address_standardizer | public | standardize_address | lextab text, gaztab text, rultab text, micro text, macro text | stdaddr - amcheck | public | bt_index_check | index regclass | void - amcheck | public | bt_index_check | index regclass, heapallindexed boolean, checkunique boolean | void - amcheck | public | bt_index_check | index regclass, heapallindexed boolean | void - amcheck | public | bt_index_parent_check | index regclass | void - amcheck | public | bt_index_parent_check | index regclass, heapallindexed boolean, rootdescend boolean, checkunique boolean | void - amcheck | public | bt_index_parent_check | index regclass, heapallindexed boolean | void - amcheck | public | bt_index_parent_check | index regclass, heapallindexed boolean, rootdescend boolean | void - amcheck | public | verify_heapam | relation regclass, on_error_stop boolean, check_toast boolean, skip text, startblock bigint, endblock bigint, OUT blkno bigint, OUT offnum integer, OUT attnum integer, OUT msg text | SETOF record + amcheck | extensions | bt_index_check | index regclass | void + amcheck | extensions | bt_index_check | index regclass, heapallindexed boolean, checkunique boolean | void + amcheck | extensions | bt_index_check | index regclass, heapallindexed boolean | void + amcheck | extensions | bt_index_parent_check | index regclass | void + amcheck | extensions | bt_index_parent_check | index regclass, heapallindexed boolean, rootdescend boolean, checkunique boolean | void + amcheck | extensions | bt_index_parent_check | index regclass, heapallindexed boolean | void + amcheck | extensions | bt_index_parent_check | index regclass, heapallindexed boolean, rootdescend boolean | void + amcheck | extensions | verify_heapam | relation regclass, on_error_stop boolean, check_toast boolean, skip text, startblock bigint, endblock bigint, OUT blkno bigint, OUT offnum integer, OUT attnum integer, OUT msg text | SETOF record autoinc | public | autoinc | | trigger bloom | public | blhandler | internal | index_am_handler btree_gin | public | gin_btree_consistent | internal, smallint, anyelement, integer, internal, internal | boolean diff --git a/nix/tests/expected/z_orioledb-17_ext_interface.out b/nix/tests/expected/z_orioledb-17_ext_interface.out index 0718c6ed38..30f09b5ede 100644 --- a/nix/tests/expected/z_orioledb-17_ext_interface.out +++ b/nix/tests/expected/z_orioledb-17_ext_interface.out @@ -163,14 +163,14 @@ order by address_standardizer | public | parse_address | text, OUT num text, OUT street text, OUT street2 text, OUT address1 text, OUT city text, OUT state text, OUT zip text, OUT zipplus text, OUT country text | record address_standardizer | public | standardize_address | lextab text, gaztab text, rultab text, address text | stdaddr address_standardizer | public | standardize_address | lextab text, gaztab text, rultab text, micro text, macro text | stdaddr - amcheck | public | bt_index_check | index regclass | void - amcheck | public | bt_index_check | index regclass, heapallindexed boolean, checkunique boolean | void - amcheck | public | bt_index_check | index regclass, heapallindexed boolean | void - amcheck | public | bt_index_parent_check | index regclass | void - amcheck | public | bt_index_parent_check | index regclass, heapallindexed boolean, rootdescend boolean, checkunique boolean | void - amcheck | public | bt_index_parent_check | index regclass, heapallindexed boolean | void - amcheck | public | bt_index_parent_check | index regclass, heapallindexed boolean, rootdescend boolean | void - amcheck | public | verify_heapam | relation regclass, on_error_stop boolean, check_toast boolean, skip text, startblock bigint, endblock bigint, OUT blkno bigint, OUT offnum integer, OUT attnum integer, OUT msg text | SETOF record + amcheck | extensions | bt_index_check | index regclass | void + amcheck | extensions | bt_index_check | index regclass, heapallindexed boolean, checkunique boolean | void + amcheck | extensions | bt_index_check | index regclass, heapallindexed boolean | void + amcheck | extensions | bt_index_parent_check | index regclass | void + amcheck | extensions | bt_index_parent_check | index regclass, heapallindexed boolean, rootdescend boolean, checkunique boolean | void + amcheck | extensions | bt_index_parent_check | index regclass, heapallindexed boolean | void + amcheck | extensions | bt_index_parent_check | index regclass, heapallindexed boolean, rootdescend boolean | void + amcheck | extensions | verify_heapam | relation regclass, on_error_stop boolean, check_toast boolean, skip text, startblock bigint, endblock bigint, OUT blkno bigint, OUT offnum integer, OUT attnum integer, OUT msg text | SETOF record autoinc | public | autoinc | | trigger bloom | public | blhandler | internal | index_am_handler btree_gin | public | gin_btree_consistent | internal, smallint, anyelement, integer, internal, internal | boolean diff --git a/nix/tests/expected/z_orioledb-17_extensions_schema.out b/nix/tests/expected/z_orioledb-17_extensions_schema.out index 2eafa8119c..4456d9ace2 100644 --- a/nix/tests/expected/z_orioledb-17_extensions_schema.out +++ b/nix/tests/expected/z_orioledb-17_extensions_schema.out @@ -15,9 +15,10 @@ order by e.extname; extension_name | schema_name | extension_owner --------------------+-------------+----------------- + amcheck | extensions | supabase_admin orioledb | extensions | supabase_admin pg_stat_statements | extensions | supabase_admin pgcrypto | extensions | supabase_admin uuid-ossp | extensions | supabase_admin -(4 rows) +(5 rows) diff --git a/nix/tests/prime-superuser.sql b/nix/tests/prime-superuser.sql index e8382c0046..435fc4fde7 100644 --- a/nix/tests/prime-superuser.sql +++ b/nix/tests/prime-superuser.sql @@ -11,10 +11,10 @@ -- ansible/files/postgresql_config: -- -- 1. "omitted because may be unsafe" — supautils.conf.j2. --- Covers the entries available in BOTH PG 15 and PG 17 builds. Three more --- entries from the same list, `adminpack`, `amcheck`, and `old_snapshot`, were --- removed from contrib in PG 17 or enabled for customers, and are loaded directly by --- nix/tests/sql/z_15_ext_interface.sql for the PG 15 path (adminpack, old_snapshot only). +-- Covers the entries available in BOTH PG 15 and PG 17 builds. Two more +-- entries from the same list, `adminpack` and `old_snapshot`, were +-- removed from contrib in PG 17 and are loaded directly by +-- nix/tests/sql/z_15_ext_interface.sql for the PG 15 path. -- -- 2. "omitted because deprecated" — supautils.conf.j2. -- Not in privileged_extensions, so non-superuser can't install. diff --git a/nix/tests/sql/amcheck.sql b/nix/tests/sql/amcheck.sql new file mode 100644 index 0000000000..89741063f8 --- /dev/null +++ b/nix/tests/sql/amcheck.sql @@ -0,0 +1,92 @@ +-- amcheck is installable by customers (PSQL-1327): it lives in +-- supautils.privileged_extensions, so the non-superuser `postgres` role can +-- `create extension amcheck` and call bt_index_check() to find corrupt indexes +-- without a full REINDEX DATABASE. +-- +-- The schema it lands in is `extensions`. amcheck deliberately +-- performs no permission check of its own -- upstream verify_nbtree.c says +-- "Intentionally not checking permissions" -- so its `REVOKE ALL ... FROM +-- PUBLIC` is the only access control, and any role holding EXECUTE can check +-- any index in the database. +-- +-- Two standing ALTER DEFAULT PRIVILEGES rules decide which role gets that EXECUTE, +-- because supautils creates privileged extensions as supabase_admin: +-- +-- * schema public -> postgres, anon, authenticated, service_role +-- (migrations/db/init-scripts/00000000000000-initial-schema.sql) +-- * schema extensions -> postgres only +-- (migrations/db/migrations/20230224042246_grant_extensions_perms_for_postgres.sql) +-- +-- Landing in public would therefore expose bt_index_parent_check() (ShareLock, +-- blocks writes) and verify_heapam() to unauthenticated PostgREST callers. So +-- supautils.extensions_parameter_overrides pins amcheck to `extensions`, and +-- this suite asserts both the placement and the resulting privilege matrix. +-- +-- Runs against the rendered supautils.conf.j2 with the real migrations applied, +-- so the roles below are the actual platform roles. + +-- the platform config pins amcheck's schema +show supautils.extensions_parameter_overrides; + +-- precondition: postgres is not a superuser, else every assertion is vacuous +select rolsuper from pg_roles where rolname = 'postgres'; + +-- prime.sql already created amcheck; drop it so the creates below are observable +drop extension if exists amcheck; + +-- a non-superuser can install it, and it lands in `extensions` rather than the +-- session search_path's first entry (public) +set role postgres; +create extension amcheck; +reset role; + +select extowner::regrole as owner, extnamespace::regnamespace as schema + from pg_extension + where extname = 'amcheck'; + +-- the override wins over an explicitly requested schema, so the placement +-- cannot be opted out of +drop extension amcheck; + +set role postgres; +create extension amcheck with schema public; +reset role; + +select extnamespace::regnamespace as schema_after_requesting_public + from pg_extension + where extname = 'amcheck'; + +-- the privilege matrix that placement produces. Aggregated over every function +-- the extension owns rather than named signatures, so this stays correct as +-- amcheck gains functions across PG versions (15 ships 6, 17 ships 8) and +-- catches any future addition that arrives with different grants. +select r.rolname, + bool_and(has_function_privilege(r.rolname, p.oid, 'execute')) as all_functions, + bool_or(has_function_privilege(r.rolname, p.oid, 'execute')) as any_function + from pg_proc p + join pg_depend d on d.objid = p.oid and d.deptype = 'e' + join pg_extension e on e.oid = d.refobjid and e.extname = 'amcheck' + cross join (values ('postgres'), ('anon'), ('authenticated'), ('service_role')) as r(rolname) + group by r.rolname + order by r.rolname; + +-- the support use case: postgres checks its own index, and -- since amcheck has +-- no per-relation gate -- one on a table it does not own, which is what makes +-- the extension useful after a 15->17 upgrade corrupts an auth index +set role postgres; +create table amcheck_heap(i int primary key) using heap; +insert into amcheck_heap select generate_series(1, 100); +select extensions.bt_index_check('amcheck_heap_pkey'::regclass); +select extensions.bt_index_check('auth.users_pkey'::regclass); +reset role; + +-- the API roles must not reach it. The denial is at the function level, so it +-- applies to every target relation, not just this one. +set role anon; +select extensions.bt_index_check('auth.users_pkey'::regclass); +reset role; + +-- restore the state prime.sql created (amcheck present, in `extensions`) +set role postgres; +drop table amcheck_heap; +reset role; From 7bf469ea2aeea2cc2fff34ebbb4cf26fdad1f218 Mon Sep 17 00:00:00 2001 From: Sam Rose Date: Tue, 18 Aug 2026 17:14:13 -0400 Subject: [PATCH 3/7] fix: bug in regression test --- nix/tests/expected/amcheck.out | 15 ++++++++++++--- nix/tests/sql/amcheck.sql | 16 +++++++++++++--- 2 files changed, 25 insertions(+), 6 deletions(-) diff --git a/nix/tests/expected/amcheck.out b/nix/tests/expected/amcheck.out index 370fd27a6f..ef4dd4429e 100644 --- a/nix/tests/expected/amcheck.out +++ b/nix/tests/expected/amcheck.out @@ -89,8 +89,16 @@ select r.rolname, (4 rows) -- the support use case: postgres checks its own index, and -- since amcheck has --- no per-relation gate -- one on a table it does not own, which is what makes --- the extension useful after a 15->17 upgrade corrupts an auth index +-- no per-relation gate -- one on a relation it neither owns nor holds any grant +-- on. That second case is what makes the extension useful when an upgrade +-- corrupts an index in a schema the customer does not control. +-- +-- The not-mine relation is a supabase_admin-owned heap table rather than a real +-- auth index: on the orioledb variant auth.users is orioledb-backed, so its +-- btree has no conventional storage and bt_index_check fails reading block 0. +create table amcheck_notmine(i int primary key) using heap; +insert into amcheck_notmine select generate_series(1, 100); +revoke all on amcheck_notmine from postgres, anon, authenticated, service_role; set role postgres; create table amcheck_heap(i int primary key) using heap; insert into amcheck_heap select generate_series(1, 100); @@ -100,7 +108,7 @@ select extensions.bt_index_check('amcheck_heap_pkey'::regclass); (1 row) -select extensions.bt_index_check('auth.users_pkey'::regclass); +select extensions.bt_index_check('amcheck_notmine_pkey'::regclass); bt_index_check ---------------- @@ -117,3 +125,4 @@ reset role; set role postgres; drop table amcheck_heap; reset role; +drop table amcheck_notmine; diff --git a/nix/tests/sql/amcheck.sql b/nix/tests/sql/amcheck.sql index 89741063f8..65ad9368c6 100644 --- a/nix/tests/sql/amcheck.sql +++ b/nix/tests/sql/amcheck.sql @@ -71,13 +71,22 @@ select r.rolname, order by r.rolname; -- the support use case: postgres checks its own index, and -- since amcheck has --- no per-relation gate -- one on a table it does not own, which is what makes --- the extension useful after a 15->17 upgrade corrupts an auth index +-- no per-relation gate -- one on a relation it neither owns nor holds any grant +-- on. That second case is what makes the extension useful when an upgrade +-- corrupts an index in a schema the customer does not control. +-- +-- The not-mine relation is a supabase_admin-owned heap table rather than a real +-- auth index: on the orioledb variant auth.users is orioledb-backed, so its +-- btree has no conventional storage and bt_index_check fails reading block 0. +create table amcheck_notmine(i int primary key) using heap; +insert into amcheck_notmine select generate_series(1, 100); +revoke all on amcheck_notmine from postgres, anon, authenticated, service_role; + set role postgres; create table amcheck_heap(i int primary key) using heap; insert into amcheck_heap select generate_series(1, 100); select extensions.bt_index_check('amcheck_heap_pkey'::regclass); -select extensions.bt_index_check('auth.users_pkey'::regclass); +select extensions.bt_index_check('amcheck_notmine_pkey'::regclass); reset role; -- the API roles must not reach it. The denial is at the function level, so it @@ -90,3 +99,4 @@ reset role; set role postgres; drop table amcheck_heap; reset role; +drop table amcheck_notmine; From 320c8cfe665e8dd0e65faac3e55d09ce7ae9a168 Mon Sep 17 00:00:00 2001 From: Sam Rose Date: Wed, 19 Aug 2026 09:07:12 -0400 Subject: [PATCH 4/7] fix: make amcheck regression test independent of the image --- .../files/postgresql_config/supautils.conf.j2 | 16 +-- nix/ext/tests/amcheck-upgrade.nix | 70 +++++------- nix/tests/expected/amcheck.out | 107 ++++++++++-------- nix/tests/sql/amcheck.sql | 104 +++++++++-------- 4 files changed, 152 insertions(+), 145 deletions(-) diff --git a/ansible/files/postgresql_config/supautils.conf.j2 b/ansible/files/postgresql_config/supautils.conf.j2 index ccd30bc999..b25ee135e5 100644 --- a/ansible/files/postgresql_config/supautils.conf.j2 +++ b/ansible/files/postgresql_config/supautils.conf.j2 @@ -1,11 +1,11 @@ -# amcheck must stay pinned to `extensions`. supautils creates privileged extensions as -# supabase_admin, whose search_path starts with `public`, and the ALTER DEFAULT PRIVILEGES in -# migrations/db/init-scripts/00000000000000-initial-schema.sql grants EXECUTE on anything -# supabase_admin creates there to anon, authenticated and service_role — i.e. to unauthenticated -# PostgREST callers. amcheck performs no permission check of its own (upstream verify_nbtree.c: -# "Intentionally not checking permissions"), so its REVOKE ALL FROM PUBLIC is the only gate and -# whoever holds EXECUTE can check any index in the database, including auth's. In `extensions` -# the equivalent rule grants to `postgres` only. See PSQL-1327 and nix/tests/sql/amcheck.sql. +# amcheck must stay pinned to extensions. supautils creates privileged extensions as +# supabase_admin, whose search_path starts with public, where ALTER DEFAULT PRIVILEGES +# (init-scripts/00000000000000-initial-schema.sql) grants EXECUTE to anon, authenticated +# and service_role, i.e. to unauthenticated PostgREST callers. amcheck does no permission +# check of its own (upstream verify_nbtree.c: "Intentionally not checking permissions"), +# so EXECUTE alone lets a role check any index in the database, including auth's. In +# extensions the equivalent rule grants to postgres only. +# See PSQL-1327 and nix/tests/sql/amcheck.sql. supautils.extensions_parameter_overrides = '{"pg_cron":{"schema":"pg_catalog"},"amcheck":{"schema":"extensions"}}' supautils.policy_grants = '{"postgres":["auth.audit_log_entries","auth.flow_state","auth.identities","auth.instances","auth.mfa_amr_claims","auth.mfa_challenges","auth.mfa_factors","auth.oauth_clients","auth.one_time_tokens","auth.refresh_tokens","auth.saml_providers","auth.saml_relay_states","auth.sessions","auth.sso_domains","auth.sso_providers","auth.users","realtime.messages","realtime.subscription","storage.buckets","storage.buckets_analytics","storage.objects","storage.prefixes","storage.s3_multipart_uploads","storage.s3_multipart_uploads_parts"]}' supautils.drop_trigger_grants = '{"postgres":["auth.audit_log_entries","auth.flow_state","auth.identities","auth.instances","auth.mfa_amr_claims","auth.mfa_challenges","auth.mfa_factors","auth.oauth_clients","auth.one_time_tokens","auth.refresh_tokens","auth.saml_providers","auth.saml_relay_states","auth.sessions","auth.sso_domains","auth.sso_providers","auth.users","realtime.messages","realtime.subscription","storage.buckets","storage.buckets_analytics","storage.objects","storage.prefixes","storage.s3_multipart_uploads","storage.s3_multipart_uploads_parts"]}' diff --git a/nix/ext/tests/amcheck-upgrade.nix b/nix/ext/tests/amcheck-upgrade.nix index 95a6ea8359..7c8af0e6bd 100644 --- a/nix/ext/tests/amcheck-upgrade.nix +++ b/nix/ext/tests/amcheck-upgrade.nix @@ -1,27 +1,23 @@ -# amcheck across a 15 -> 17 pg_upgrade (PSQL-1327). +# amcheck across a 15 to 17 pg_upgrade (PSQL-1327). # -# amcheck is a customer-installable privileged extension pinned to the -# `extensions` schema by supautils.extensions_parameter_overrides. That pin is -# what keeps EXECUTE away from anon/authenticated/service_role: in `public` the -# ALTER DEFAULT PRIVILEGES in initial-schema.sql would grant to all four roles, -# and amcheck performs no permission check of its own, so a grant is a licence -# to check any index in the database. +# amcheck is pinned to the extensions schema by +# supautils.extensions_parameter_overrides, which is what keeps EXECUTE away from +# anon, authenticated and service_role. nix/tests/sql/amcheck.sql asserts that on +# a fresh install. # -# nix/tests/sql/amcheck.sql asserts that on a fresh install. This test covers -# the part pg_regress cannot reach: whether the schema and the grants survive a -# major-version upgrade. That matters because extension-member function ACLs -# ride on pg_init_privs through pg_upgrade's dump/restore, and because 15 ships -# amcheck 1.3 while 17 ships 1.4 -- so the upgrade also runs an ALTER EXTENSION -# UPDATE that creates new functions, each of which picks up whatever default -# privileges apply at that moment. +# This test covers what pg_regress cannot reach: whether the schema and the +# grants survive a major version upgrade. Two things could break it. Extension +# member function ACLs ride on pg_init_privs through pg_upgrade's dump and +# restore. And 15 ships amcheck 1.3 while 17 ships 1.4, so the upgrade also runs +# an ALTER EXTENSION UPDATE that creates new functions, each picking up whatever +# default privileges apply at that moment. # -# Upgraded projects are exactly the population the ticket is about (customers -# hitting corrupt indexes after 15 -> 17), so a silent re-grant here would undo -# the fix for everyone who matters. +# Upgraded projects are the population the ticket is about (customers hitting +# corrupt indexes after a 15 to 17 upgrade), so a silent re-grant here would undo +# the fix for exactly the people it was written for. # -# Deliberately 15 -> 17 only. There is no upgrade path into orioledb (its -# specialisation wipes the data directory), so an orioledb leg would assert -# nothing about upgrade behaviour and only add VM runtime. +# 15 to 17 only. There is no upgrade path into orioledb, since its specialisation +# wipes the data directory, so an orioledb leg would assert nothing here. { self, pkgs }: let testLib = import ./lib.nix { inherit self pkgs; }; @@ -50,10 +46,9 @@ pkgs.testers.runNixOSTest { '' pg17_configuration = "${pg17-configuration}" - # postgres holds EXECUTE (with grant option, via the extensions-schema - # default privileges); the three PostgREST roles hold nothing. Aggregated - # over every function the extension owns rather than named signatures, so - # this stays correct as amcheck grows from 6 functions on 15 to 8 on 17. + # postgres holds EXECUTE via the extensions schema default privileges; + # the three PostgREST roles hold nothing. Aggregated over every function + # the extension owns, so it holds as amcheck grows from 6 on 15 to 8 on 17. EXPECTED_ACL = "anon,f,f\nauthenticated,f,f\npostgres,t,t\nservice_role,f,f" ACL_QUERY = ( @@ -73,10 +68,9 @@ pkgs.testers.runNixOSTest { ) def sql(query, role=None): - # Connect as supabase_admin (the bootstrap superuser this harness - # authenticates as) and pick up the target role via the startup packet, - # so each statement runs under its own session rather than being bundled - # into one implicit transaction by a multi-statement -c. + # Connect as supabase_admin (the superuser this harness authenticates + # as) and set the target role via the startup packet, so each statement + # gets its own session instead of one implicit multi-statement -c. prefix = "" if role is not None: prefix = "PGOPTIONS=\"-c role=" + role + "\" " @@ -108,8 +102,8 @@ pkgs.testers.runNixOSTest { assert sql("select rolsuper from pg_roles where rolname = 'postgres'") == "f", ( "postgres is a superuser; every grant assertion below would be vacuous" ) - # Fail loudly if the role-switching mechanism itself is broken, rather - # than silently running the whole suite as supabase_admin. + # Fail loudly if role switching is broken, rather than silently + # running the whole suite as supabase_admin. whoami = sql("select current_user", role="postgres") assert whoami == "postgres", f"expected to be acting as postgres, got: {whoami}" @@ -136,11 +130,10 @@ pkgs.testers.runNixOSTest { assert acl_after == EXPECTED_ACL, f"grants changed across pg_upgrade:\n{acl_after}" with subtest("The 1.3 -> 1.4 extension update preserves the grants"): - # The platform runs pg_upgrade's generated update_extensions.sql after - # the upgrade (admin_api_scripts/pg_upgrade_scripts/complete.sh). This - # harness runs raw pg_upgrade, so replay that file when it is present - # and fall back to an explicit ALTER when it is not -- either route - # creates the new 1.4 functions, which is what we need to inspect. + # The platform replays pg_upgrade's generated update_extensions.sql + # (admin_api_scripts/pg_upgrade_scripts/complete.sh). This harness runs + # raw pg_upgrade, so replay that file if present, else ALTER explicitly. + # Either route creates the new 1.4 functions, which is what we inspect. has_script = server.succeed( "test -f /var/lib/postgresql/update_extensions.sql && echo yes || echo no" ).strip() @@ -160,9 +153,8 @@ pkgs.testers.runNixOSTest { assert installed_version == default_version, ( f"expected amcheck at PG 17's default {default_version}, got {installed_version}" ) - # Guard against this subtest quietly becoming a no-op if 15 and 17 ever - # ship the same amcheck version -- there would then be no new functions - # to acquire grants, and nothing here would be under test. + # If 15 and 17 ever ship the same amcheck version there are no new + # functions to acquire grants, and this subtest silently tests nothing. assert installed_version != before.split(",")[0], ( f"amcheck was already at {installed_version} on PG 15; this subtest no " "longer exercises a version bump and needs rewriting" @@ -183,7 +175,7 @@ pkgs.testers.runNixOSTest { role="postgres", ) sql("select extensions.bt_index_check('amcheck_heap_pkey'::regclass)", role="postgres") - # amcheck has no per-relation gate, which is the point: a customer can + # amcheck has no per relation gate, which is the point: a customer can # check an auth index they do not own after an upgrade corrupts it. sql("select extensions.bt_index_check('auth.users_pkey'::regclass)", role="postgres") diff --git a/nix/tests/expected/amcheck.out b/nix/tests/expected/amcheck.out index ef4dd4429e..6798cd0b19 100644 --- a/nix/tests/expected/amcheck.out +++ b/nix/tests/expected/amcheck.out @@ -1,37 +1,34 @@ --- amcheck is installable by customers (PSQL-1327): it lives in --- supautils.privileged_extensions, so the non-superuser `postgres` role can --- `create extension amcheck` and call bt_index_check() to find corrupt indexes --- without a full REINDEX DATABASE. +-- amcheck for customers (PSQL-1327). -- --- The schema it lands in is `extensions`. amcheck deliberately --- performs no permission check of its own -- upstream verify_nbtree.c says --- "Intentionally not checking permissions" -- so its `REVOKE ALL ... FROM --- PUBLIC` is the only access control, and any role holding EXECUTE can check --- any index in the database. +-- amcheck is in supautils.privileged_extensions, so the non-superuser postgres +-- role can install it and call bt_index_check() instead of running a full +-- REINDEX DATABASE. -- --- Two standing ALTER DEFAULT PRIVILEGES rules decide which role gets that EXECUTE, --- because supautils creates privileged extensions as supabase_admin: +-- The schema it lands in decides who can call it. supautils creates privileged +-- extensions as supabase_admin, and ALTER DEFAULT PRIVILEGES grants EXECUTE on +-- whatever supabase_admin creates: -- --- * schema public -> postgres, anon, authenticated, service_role --- (migrations/db/init-scripts/00000000000000-initial-schema.sql) --- * schema extensions -> postgres only --- (migrations/db/migrations/20230224042246_grant_extensions_perms_for_postgres.sql) +-- in public: to postgres, anon, authenticated, service_role +-- (init-scripts/00000000000000-initial-schema.sql) +-- in extensions: to postgres only +-- (migrations/20230224042246_grant_extensions_perms_for_postgres.sql) -- --- Landing in public would therefore expose bt_index_parent_check() (ShareLock, --- blocks writes) and verify_heapam() to unauthenticated PostgREST callers. So --- supautils.extensions_parameter_overrides pins amcheck to `extensions`, and --- this suite asserts both the placement and the resulting privilege matrix. +-- amcheck does no permission check of its own. Upstream verify_nbtree.c says +-- "Intentionally not checking permissions", so EXECUTE alone lets a role check +-- any index in the database. Landing in public would expose +-- bt_index_parent_check() (takes ShareLock, blocks writes) and verify_heapam() +-- to unauthenticated PostgREST callers. -- --- Runs against the rendered supautils.conf.j2 with the real migrations applied, --- so the roles below are the actual platform roles. --- the platform config pins amcheck's schema +-- supautils.extensions_parameter_overrides pins amcheck to extensions. This +-- suite asserts that placement and the privilege matrix it produces. +-- the pin lives in the platform config show supautils.extensions_parameter_overrides; supautils.extensions_parameter_overrides ----------------------------------------------------------------------- {"pg_cron":{"schema":"pg_catalog"},"amcheck":{"schema":"extensions"}} (1 row) --- precondition: postgres is not a superuser, else every assertion is vacuous +-- precondition: postgres must not be a superuser, or nothing below is meaningful select rolsuper from pg_roles where rolname = 'postgres'; rolsuper ---------- @@ -40,8 +37,7 @@ select rolsuper from pg_roles where rolname = 'postgres'; -- prime.sql already created amcheck; drop it so the creates below are observable drop extension if exists amcheck; --- a non-superuser can install it, and it lands in `extensions` rather than the --- session search_path's first entry (public) +-- a non-superuser can install it, and it lands in extensions rather than public set role postgres; create extension amcheck; reset role; @@ -53,8 +49,7 @@ select extowner::regrole as owner, extnamespace::regnamespace as schema supabase_admin | extensions (1 row) --- the override wins over an explicitly requested schema, so the placement --- cannot be opted out of +-- the override wins over an explicitly requested schema drop extension amcheck; set role postgres; create extension amcheck with schema public; @@ -67,10 +62,9 @@ select extnamespace::regnamespace as schema_after_requesting_public extensions (1 row) --- the privilege matrix that placement produces. Aggregated over every function --- the extension owns rather than named signatures, so this stays correct as --- amcheck gains functions across PG versions (15 ships 6, 17 ships 8) and --- catches any future addition that arrives with different grants. +-- The privilege matrix. Aggregated over every function the extension owns rather +-- than named signatures, so one expected file covers PG 15 (6 functions) and +-- PG 17 (8), and functions added by future versions are picked up automatically. select r.rolname, bool_and(has_function_privilege(r.rolname, p.oid, 'execute')) as all_functions, bool_or(has_function_privilege(r.rolname, p.oid, 'execute')) as any_function @@ -88,27 +82,41 @@ select r.rolname, service_role | f | f (4 rows) --- the support use case: postgres checks its own index, and -- since amcheck has --- no per-relation gate -- one on a relation it neither owns nor holds any grant --- on. That second case is what makes the extension useful when an upgrade --- corrupts an index in a schema the customer does not control. +-- The support case: postgres checks an index it owns, then one it does not. +-- amcheck has no per relation gate, which is what makes it useful when an +-- upgrade corrupts an index in a schema the customer does not control. -- --- The not-mine relation is a supabase_admin-owned heap table rather than a real --- auth index: on the orioledb variant auth.users is orioledb-backed, so its --- btree has no conventional storage and bt_index_check fails reading block 0. -create table amcheck_notmine(i int primary key) using heap; -insert into amcheck_notmine select generate_series(1, 100); -revoke all on amcheck_notmine from postgres, anon, authenticated, service_role; +-- Both tables are heap tables in a schema created here, rather than existing +-- relations in public or auth. On orioledb, auth.users is orioledb backed and +-- its btree has no conventional storage. On multigres, postgres is not the +-- database owner and has no CREATE on public. +create schema amcheck_test; +grant usage, create on schema amcheck_test to postgres; +create table amcheck_test.notmine(i int primary key) using heap; +insert into amcheck_test.notmine select generate_series(1, 100); +revoke all on amcheck_test.notmine from postgres, anon, authenticated, service_role; +-- postgres neither owns this table nor can write to it. It keeps SELECT via +-- pg_read_all_data (migrations/20240606060239_grant_predefined_roles_to_postgres.sql), +-- which no revoke undoes. Ownership is the property that matters here. +select pg_get_userbyid(relowner) as notmine_owner, + has_table_privilege('postgres', 'amcheck_test.notmine', 'insert') as postgres_can_write + from pg_class + where oid = 'amcheck_test.notmine'::regclass; + notmine_owner | postgres_can_write +----------------+-------------------- + supabase_admin | f +(1 row) + set role postgres; -create table amcheck_heap(i int primary key) using heap; -insert into amcheck_heap select generate_series(1, 100); -select extensions.bt_index_check('amcheck_heap_pkey'::regclass); +create table amcheck_test.mine(i int primary key) using heap; +insert into amcheck_test.mine select generate_series(1, 100); +select extensions.bt_index_check('amcheck_test.mine_pkey'::regclass); bt_index_check ---------------- (1 row) -select extensions.bt_index_check('amcheck_notmine_pkey'::regclass); +select extensions.bt_index_check('amcheck_test.notmine_pkey'::regclass); bt_index_check ---------------- @@ -116,13 +124,14 @@ select extensions.bt_index_check('amcheck_notmine_pkey'::regclass); reset role; -- the API roles must not reach it. The denial is at the function level, so it --- applies to every target relation, not just this one. +-- applies to every target relation. set role anon; select extensions.bt_index_check('auth.users_pkey'::regclass); ERROR: permission denied for function bt_index_check reset role; --- restore the state prime.sql created (amcheck present, in `extensions`) +-- restore the state prime.sql created (amcheck present, in extensions) set role postgres; -drop table amcheck_heap; +drop table amcheck_test.mine; reset role; -drop table amcheck_notmine; +drop table amcheck_test.notmine; +drop schema amcheck_test; diff --git a/nix/tests/sql/amcheck.sql b/nix/tests/sql/amcheck.sql index 65ad9368c6..63956acdb8 100644 --- a/nix/tests/sql/amcheck.sql +++ b/nix/tests/sql/amcheck.sql @@ -1,41 +1,37 @@ --- amcheck is installable by customers (PSQL-1327): it lives in --- supautils.privileged_extensions, so the non-superuser `postgres` role can --- `create extension amcheck` and call bt_index_check() to find corrupt indexes --- without a full REINDEX DATABASE. +-- amcheck for customers (PSQL-1327). -- --- The schema it lands in is `extensions`. amcheck deliberately --- performs no permission check of its own -- upstream verify_nbtree.c says --- "Intentionally not checking permissions" -- so its `REVOKE ALL ... FROM --- PUBLIC` is the only access control, and any role holding EXECUTE can check --- any index in the database. +-- amcheck is in supautils.privileged_extensions, so the non-superuser postgres +-- role can install it and call bt_index_check() instead of running a full +-- REINDEX DATABASE. -- --- Two standing ALTER DEFAULT PRIVILEGES rules decide which role gets that EXECUTE, --- because supautils creates privileged extensions as supabase_admin: +-- The schema it lands in decides who can call it. supautils creates privileged +-- extensions as supabase_admin, and ALTER DEFAULT PRIVILEGES grants EXECUTE on +-- whatever supabase_admin creates: -- --- * schema public -> postgres, anon, authenticated, service_role --- (migrations/db/init-scripts/00000000000000-initial-schema.sql) --- * schema extensions -> postgres only --- (migrations/db/migrations/20230224042246_grant_extensions_perms_for_postgres.sql) +-- in public: to postgres, anon, authenticated, service_role +-- (init-scripts/00000000000000-initial-schema.sql) +-- in extensions: to postgres only +-- (migrations/20230224042246_grant_extensions_perms_for_postgres.sql) -- --- Landing in public would therefore expose bt_index_parent_check() (ShareLock, --- blocks writes) and verify_heapam() to unauthenticated PostgREST callers. So --- supautils.extensions_parameter_overrides pins amcheck to `extensions`, and --- this suite asserts both the placement and the resulting privilege matrix. +-- amcheck does no permission check of its own. Upstream verify_nbtree.c says +-- "Intentionally not checking permissions", so EXECUTE alone lets a role check +-- any index in the database. Landing in public would expose +-- bt_index_parent_check() (takes ShareLock, blocks writes) and verify_heapam() +-- to unauthenticated PostgREST callers. -- --- Runs against the rendered supautils.conf.j2 with the real migrations applied, --- so the roles below are the actual platform roles. +-- supautils.extensions_parameter_overrides pins amcheck to extensions. This +-- suite asserts that placement and the privilege matrix it produces. --- the platform config pins amcheck's schema +-- the pin lives in the platform config show supautils.extensions_parameter_overrides; --- precondition: postgres is not a superuser, else every assertion is vacuous +-- precondition: postgres must not be a superuser, or nothing below is meaningful select rolsuper from pg_roles where rolname = 'postgres'; -- prime.sql already created amcheck; drop it so the creates below are observable drop extension if exists amcheck; --- a non-superuser can install it, and it lands in `extensions` rather than the --- session search_path's first entry (public) +-- a non-superuser can install it, and it lands in extensions rather than public set role postgres; create extension amcheck; reset role; @@ -44,8 +40,7 @@ select extowner::regrole as owner, extnamespace::regnamespace as schema from pg_extension where extname = 'amcheck'; --- the override wins over an explicitly requested schema, so the placement --- cannot be opted out of +-- the override wins over an explicitly requested schema drop extension amcheck; set role postgres; @@ -56,10 +51,9 @@ select extnamespace::regnamespace as schema_after_requesting_public from pg_extension where extname = 'amcheck'; --- the privilege matrix that placement produces. Aggregated over every function --- the extension owns rather than named signatures, so this stays correct as --- amcheck gains functions across PG versions (15 ships 6, 17 ships 8) and --- catches any future addition that arrives with different grants. +-- The privilege matrix. Aggregated over every function the extension owns rather +-- than named signatures, so one expected file covers PG 15 (6 functions) and +-- PG 17 (8), and functions added by future versions are picked up automatically. select r.rolname, bool_and(has_function_privilege(r.rolname, p.oid, 'execute')) as all_functions, bool_or(has_function_privilege(r.rolname, p.oid, 'execute')) as any_function @@ -70,33 +64,45 @@ select r.rolname, group by r.rolname order by r.rolname; --- the support use case: postgres checks its own index, and -- since amcheck has --- no per-relation gate -- one on a relation it neither owns nor holds any grant --- on. That second case is what makes the extension useful when an upgrade --- corrupts an index in a schema the customer does not control. +-- The support case: postgres checks an index it owns, then one it does not. +-- amcheck has no per relation gate, which is what makes it useful when an +-- upgrade corrupts an index in a schema the customer does not control. -- --- The not-mine relation is a supabase_admin-owned heap table rather than a real --- auth index: on the orioledb variant auth.users is orioledb-backed, so its --- btree has no conventional storage and bt_index_check fails reading block 0. -create table amcheck_notmine(i int primary key) using heap; -insert into amcheck_notmine select generate_series(1, 100); -revoke all on amcheck_notmine from postgres, anon, authenticated, service_role; +-- Both tables are heap tables in a schema created here, rather than existing +-- relations in public or auth. On orioledb, auth.users is orioledb backed and +-- its btree has no conventional storage. On multigres, postgres is not the +-- database owner and has no CREATE on public. +create schema amcheck_test; +grant usage, create on schema amcheck_test to postgres; + +create table amcheck_test.notmine(i int primary key) using heap; +insert into amcheck_test.notmine select generate_series(1, 100); +revoke all on amcheck_test.notmine from postgres, anon, authenticated, service_role; + +-- postgres neither owns this table nor can write to it. It keeps SELECT via +-- pg_read_all_data (migrations/20240606060239_grant_predefined_roles_to_postgres.sql), +-- which no revoke undoes. Ownership is the property that matters here. +select pg_get_userbyid(relowner) as notmine_owner, + has_table_privilege('postgres', 'amcheck_test.notmine', 'insert') as postgres_can_write + from pg_class + where oid = 'amcheck_test.notmine'::regclass; set role postgres; -create table amcheck_heap(i int primary key) using heap; -insert into amcheck_heap select generate_series(1, 100); -select extensions.bt_index_check('amcheck_heap_pkey'::regclass); -select extensions.bt_index_check('amcheck_notmine_pkey'::regclass); +create table amcheck_test.mine(i int primary key) using heap; +insert into amcheck_test.mine select generate_series(1, 100); +select extensions.bt_index_check('amcheck_test.mine_pkey'::regclass); +select extensions.bt_index_check('amcheck_test.notmine_pkey'::regclass); reset role; -- the API roles must not reach it. The denial is at the function level, so it --- applies to every target relation, not just this one. +-- applies to every target relation. set role anon; select extensions.bt_index_check('auth.users_pkey'::regclass); reset role; --- restore the state prime.sql created (amcheck present, in `extensions`) +-- restore the state prime.sql created (amcheck present, in extensions) set role postgres; -drop table amcheck_heap; +drop table amcheck_test.mine; reset role; -drop table amcheck_notmine; +drop table amcheck_test.notmine; +drop schema amcheck_test; From 122ac12997274b34f1aff08631cabf1a62af3f26 Mon Sep 17 00:00:00 2001 From: Sam Rose Date: Wed, 19 Aug 2026 17:05:04 -0400 Subject: [PATCH 5/7] chore: bump to test ami --- ansible/vars.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ansible/vars.yml b/ansible/vars.yml index 930129c95e..77b3d8cf0f 100644 --- a/ansible/vars.yml +++ b/ansible/vars.yml @@ -11,9 +11,9 @@ postgres_major: # This is the source of truth for Postgres versions used in the Dockerfiles, and # is used to derive image tags and base images in the release matrix. postgres_release: - postgresorioledb-17: "17.9.0.018-orioledb" - postgres17: "17.6.1.165" - postgres15: "15.14.1.165" + postgresorioledb-17: "17.9.0.018-orioledb-amcheck-1" + postgres17: "17.6.1.165-amcheck-1" + postgres15: "15.14.1.165-amcheck-1" # Docker release matrix — base images built first, layered images built on top. # tag and base_tag are derived at build time from postgres_release via release_key. # tag_suffix is appended to the release version to form the final image tag. From 56b863cbd0d034321d25aca08dcd86770d0acac9 Mon Sep 17 00:00:00 2001 From: Sam Rose Date: Fri, 21 Aug 2026 15:34:08 -0400 Subject: [PATCH 6/7] fix: shift to pg_catalog schema --- .../files/postgresql_config/supautils.conf.j2 | 10 +----- .../amcheck/after-create.sql | 19 ++++++++++ nix/ext/tests/amcheck-upgrade.nix | 20 +++++------ nix/tests/sql/amcheck.sql | 35 ++++++++----------- 4 files changed, 45 insertions(+), 39 deletions(-) create mode 100644 ansible/files/postgresql_extension_custom_scripts/amcheck/after-create.sql diff --git a/ansible/files/postgresql_config/supautils.conf.j2 b/ansible/files/postgresql_config/supautils.conf.j2 index b25ee135e5..63c10761b5 100644 --- a/ansible/files/postgresql_config/supautils.conf.j2 +++ b/ansible/files/postgresql_config/supautils.conf.j2 @@ -1,12 +1,4 @@ -# amcheck must stay pinned to extensions. supautils creates privileged extensions as -# supabase_admin, whose search_path starts with public, where ALTER DEFAULT PRIVILEGES -# (init-scripts/00000000000000-initial-schema.sql) grants EXECUTE to anon, authenticated -# and service_role, i.e. to unauthenticated PostgREST callers. amcheck does no permission -# check of its own (upstream verify_nbtree.c: "Intentionally not checking permissions"), -# so EXECUTE alone lets a role check any index in the database, including auth's. In -# extensions the equivalent rule grants to postgres only. -# See PSQL-1327 and nix/tests/sql/amcheck.sql. -supautils.extensions_parameter_overrides = '{"pg_cron":{"schema":"pg_catalog"},"amcheck":{"schema":"extensions"}}' +supautils.extensions_parameter_overrides = '{"pg_cron":{"schema":"pg_catalog"},"amcheck":{"schema":"pg_catalog"}}' supautils.policy_grants = '{"postgres":["auth.audit_log_entries","auth.flow_state","auth.identities","auth.instances","auth.mfa_amr_claims","auth.mfa_challenges","auth.mfa_factors","auth.oauth_clients","auth.one_time_tokens","auth.refresh_tokens","auth.saml_providers","auth.saml_relay_states","auth.sessions","auth.sso_domains","auth.sso_providers","auth.users","realtime.messages","realtime.subscription","storage.buckets","storage.buckets_analytics","storage.objects","storage.prefixes","storage.s3_multipart_uploads","storage.s3_multipart_uploads_parts"]}' supautils.drop_trigger_grants = '{"postgres":["auth.audit_log_entries","auth.flow_state","auth.identities","auth.instances","auth.mfa_amr_claims","auth.mfa_challenges","auth.mfa_factors","auth.oauth_clients","auth.one_time_tokens","auth.refresh_tokens","auth.saml_providers","auth.saml_relay_states","auth.sessions","auth.sso_domains","auth.sso_providers","auth.users","realtime.messages","realtime.subscription","storage.buckets","storage.buckets_analytics","storage.objects","storage.prefixes","storage.s3_multipart_uploads","storage.s3_multipart_uploads_parts"]}' # full list: address_standardizer, address_standardizer_data_us, adminpack, amcheck, autoinc, bloom, btree_gin, btree_gist, citext, cube, dblink, dict_int, dict_xsyn, earthdistance, file_fdw, fuzzystrmatch, hstore, http, hypopg, index_advisor, insert_username, intagg, intarray, isn, lo, ltree, moddatetime, old_snapshot, orioledb, pageinspect, pg_buffercache, pg_cron, pg_freespacemap, pg_graphql, pg_hashids, pg_jsonschema, pg_net, pg_prewarm, pg_repack, pg_stat_monitor, pg_stat_statements, pg_surgery, pg_tle, pg_trgm, pg_visibility, pg_walinspect, pgaudit, pgcrypto, pgjwt, pgmq, pgroonga, pgroonga_database, pgrouting, pgrowlocks, pgsodium, pgstattuple, pgtap, plcoffee, pljava, plls, plpgsql, plpgsql_check, plv8, postgis, postgis_raster, postgis_sfcgal, postgis_tiger_geocoder, postgis_topology, postgres_fdw, refint, rum, seg, sslinfo, supabase_vault, supautils, tablefunc, tcn, timescaledb, tsm_system_rows, tsm_system_time, unaccent, uuid-ossp, vector, wrappers, xml2 diff --git a/ansible/files/postgresql_extension_custom_scripts/amcheck/after-create.sql b/ansible/files/postgresql_extension_custom_scripts/amcheck/after-create.sql new file mode 100644 index 0000000000..9190471e92 --- /dev/null +++ b/ansible/files/postgresql_extension_custom_scripts/amcheck/after-create.sql @@ -0,0 +1,19 @@ +do $$ +declare + saved_search_path text := (select current_setting('search_path')); + r record; +begin + perform set_config('search_path', '', true); + + for r in + select p.oid::regprocedure as sig + from pg_depend d + join pg_proc p on p.oid = d.objid and d.classid = 'pg_proc'::regclass + join pg_extension e on e.oid = d.refobjid + where e.extname = 'amcheck' and d.deptype = 'e' + loop + execute format('grant execute on function %s to postgres', r.sig); + end loop; + + perform set_config('search_path', saved_search_path, true); +end $$; diff --git a/nix/ext/tests/amcheck-upgrade.nix b/nix/ext/tests/amcheck-upgrade.nix index 7c8af0e6bd..f91da378fd 100644 --- a/nix/ext/tests/amcheck-upgrade.nix +++ b/nix/ext/tests/amcheck-upgrade.nix @@ -1,6 +1,6 @@ # amcheck across a 15 to 17 pg_upgrade (PSQL-1327). # -# amcheck is pinned to the extensions schema by +# amcheck is pinned to the pg_catalog schema by # supautils.extensions_parameter_overrides, which is what keeps EXECUTE away from # anon, authenticated and service_role. nix/tests/sql/amcheck.sql asserts that on # a fresh install. @@ -46,7 +46,7 @@ pkgs.testers.runNixOSTest { '' pg17_configuration = "${pg17-configuration}" - # postgres holds EXECUTE via the extensions schema default privileges; + # postgres holds EXECUTE on amcheck (installed into pg_catalog by supautils); # the three PostgREST roles hold nothing. Aggregated over every function # the extension owns, so it holds as amcheck grows from 6 on 15 to 8 on 17. EXPECTED_ACL = "anon,f,f\nauthenticated,f,f\npostgres,t,t\nservice_role,f,f" @@ -107,11 +107,11 @@ pkgs.testers.runNixOSTest { whoami = sql("select current_user", role="postgres") assert whoami == "postgres", f"expected to be acting as postgres, got: {whoami}" - with subtest("A non-superuser installs amcheck on PG 15, pinned to extensions"): + with subtest("A non-superuser installs amcheck on PG 15, pinned to pg_catalog"): sql("create extension amcheck", role="postgres") before = sql(IDENTITY_QUERY) - assert before.endswith(",extensions"), ( - f"expected amcheck in the extensions schema, got: {before}" + assert before.endswith(",pg_catalog"), ( + f"expected amcheck in the pg_catalog schema, got: {before}" ) print(f"PG 15 amcheck: {before}") @@ -159,8 +159,8 @@ pkgs.testers.runNixOSTest { f"amcheck was already at {installed_version} on PG 15; this subtest no " "longer exercises a version bump and needs rewriting" ) - assert updated.endswith(",extensions"), ( - f"amcheck left the extensions schema during the update: {updated}" + assert updated.endswith(",pg_catalog"), ( + f"amcheck left the pg_catalog schema during the update: {updated}" ) acl_updated = sql(ACL_QUERY) @@ -174,15 +174,15 @@ pkgs.testers.runNixOSTest { "insert into amcheck_heap select generate_series(1, 100)", role="postgres", ) - sql("select extensions.bt_index_check('amcheck_heap_pkey'::regclass)", role="postgres") + sql("select pg_catalog.bt_index_check('amcheck_heap_pkey'::regclass)", role="postgres") # amcheck has no per relation gate, which is the point: a customer can # check an auth index they do not own after an upgrade corrupts it. - sql("select extensions.bt_index_check('auth.users_pkey'::regclass)", role="postgres") + sql("select pg_catalog.bt_index_check('auth.users_pkey'::regclass)", role="postgres") with subtest("The API roles are still locked out after the upgrade"): for role in ["anon", "authenticated", "service_role"]: err = sql_fails( - "select extensions.bt_index_check('auth.users_pkey'::regclass)", role=role + "select pg_catalog.bt_index_check('auth.users_pkey'::regclass)", role=role ) assert "permission denied for function bt_index_check" in err, ( f"expected {role} to be denied, got: {err}" diff --git a/nix/tests/sql/amcheck.sql b/nix/tests/sql/amcheck.sql index 63956acdb8..04ee5aa452 100644 --- a/nix/tests/sql/amcheck.sql +++ b/nix/tests/sql/amcheck.sql @@ -4,23 +4,18 @@ -- role can install it and call bt_index_check() instead of running a full -- REINDEX DATABASE. -- --- The schema it lands in decides who can call it. supautils creates privileged --- extensions as supabase_admin, and ALTER DEFAULT PRIVILEGES grants EXECUTE on --- whatever supabase_admin creates: +-- supautils.extensions_parameter_overrides pins it to pg_catalog, the same as pg_cron. -- --- in public: to postgres, anon, authenticated, service_role --- (init-scripts/00000000000000-initial-schema.sql) --- in extensions: to postgres only --- (migrations/20230224042246_grant_extensions_perms_for_postgres.sql) +-- Why not public: supautils creates privileged extensions as supabase_admin, and +-- ALTER DEFAULT PRIVILEGES (init-scripts/00000000000000-initial-schema.sql) grants +-- EXECUTE on anything supabase_admin creates in public to anon, authenticated and +-- service_role, the PostgREST API roles. amcheck does no permission check of its own +-- (upstream verify_nbtree.c: "Intentionally not checking permissions"), so in public +-- it would be callable over the REST API against any index, including auth's. +-- pg_catalog has no such default-privilege grant, so the API roles never get access. -- --- amcheck does no permission check of its own. Upstream verify_nbtree.c says --- "Intentionally not checking permissions", so EXECUTE alone lets a role check --- any index in the database. Landing in public would expose --- bt_index_parent_check() (takes ShareLock, blocks writes) and verify_heapam() --- to unauthenticated PostgREST callers. --- --- supautils.extensions_parameter_overrides pins amcheck to extensions. This --- suite asserts that placement and the privilege matrix it produces. +-- This suite asserts the placement (pg_catalog), that postgres can use it, and that +-- the API roles cannot. -- the pin lives in the platform config show supautils.extensions_parameter_overrides; @@ -31,7 +26,7 @@ select rolsuper from pg_roles where rolname = 'postgres'; -- prime.sql already created amcheck; drop it so the creates below are observable drop extension if exists amcheck; --- a non-superuser can install it, and it lands in extensions rather than public +-- a non-superuser can install it, and it lands in pg_catalog rather than public set role postgres; create extension amcheck; reset role; @@ -90,17 +85,17 @@ select pg_get_userbyid(relowner) as notmine_owner, set role postgres; create table amcheck_test.mine(i int primary key) using heap; insert into amcheck_test.mine select generate_series(1, 100); -select extensions.bt_index_check('amcheck_test.mine_pkey'::regclass); -select extensions.bt_index_check('amcheck_test.notmine_pkey'::regclass); +select pg_catalog.bt_index_check('amcheck_test.mine_pkey'::regclass); +select pg_catalog.bt_index_check('amcheck_test.notmine_pkey'::regclass); reset role; -- the API roles must not reach it. The denial is at the function level, so it -- applies to every target relation. set role anon; -select extensions.bt_index_check('auth.users_pkey'::regclass); +select pg_catalog.bt_index_check('auth.users_pkey'::regclass); reset role; --- restore the state prime.sql created (amcheck present, in extensions) +-- restore the state prime.sql created (amcheck present, in pg_catalog) set role postgres; drop table amcheck_test.mine; reset role; From 673cab343d18c4060d26a699cad052b4364f5a8a Mon Sep 17 00:00:00 2001 From: Sam Rose Date: Fri, 21 Aug 2026 19:11:51 -0400 Subject: [PATCH 7/7] fix: regress tests --- nix/tests/expected/amcheck.out | 41 ++++++++----------- nix/tests/expected/extensions_schema.out | 3 +- nix/tests/expected/z_15_ext_interface.out | 12 +++--- nix/tests/expected/z_15_roles.out | 8 +++- nix/tests/expected/z_17_ext_interface.out | 16 ++++---- nix/tests/expected/z_17_roles.out | 10 ++++- .../expected/z_orioledb-17_ext_interface.out | 16 ++++---- .../z_orioledb-17_extensions_schema.out | 3 +- 8 files changed, 58 insertions(+), 51 deletions(-) diff --git a/nix/tests/expected/amcheck.out b/nix/tests/expected/amcheck.out index 6798cd0b19..3f74e02046 100644 --- a/nix/tests/expected/amcheck.out +++ b/nix/tests/expected/amcheck.out @@ -4,28 +4,23 @@ -- role can install it and call bt_index_check() instead of running a full -- REINDEX DATABASE. -- --- The schema it lands in decides who can call it. supautils creates privileged --- extensions as supabase_admin, and ALTER DEFAULT PRIVILEGES grants EXECUTE on --- whatever supabase_admin creates: +-- supautils.extensions_parameter_overrides pins it to pg_catalog, the same as pg_cron. -- --- in public: to postgres, anon, authenticated, service_role --- (init-scripts/00000000000000-initial-schema.sql) --- in extensions: to postgres only --- (migrations/20230224042246_grant_extensions_perms_for_postgres.sql) +-- Why not public: supautils creates privileged extensions as supabase_admin, and +-- ALTER DEFAULT PRIVILEGES (init-scripts/00000000000000-initial-schema.sql) grants +-- EXECUTE on anything supabase_admin creates in public to anon, authenticated and +-- service_role, the PostgREST API roles. amcheck does no permission check of its own +-- (upstream verify_nbtree.c: "Intentionally not checking permissions"), so in public +-- it would be callable over the REST API against any index, including auth's. +-- pg_catalog has no such default-privilege grant, so the API roles never get access. -- --- amcheck does no permission check of its own. Upstream verify_nbtree.c says --- "Intentionally not checking permissions", so EXECUTE alone lets a role check --- any index in the database. Landing in public would expose --- bt_index_parent_check() (takes ShareLock, blocks writes) and verify_heapam() --- to unauthenticated PostgREST callers. --- --- supautils.extensions_parameter_overrides pins amcheck to extensions. This --- suite asserts that placement and the privilege matrix it produces. +-- This suite asserts the placement (pg_catalog), that postgres can use it, and that +-- the API roles cannot. -- the pin lives in the platform config show supautils.extensions_parameter_overrides; supautils.extensions_parameter_overrides ----------------------------------------------------------------------- - {"pg_cron":{"schema":"pg_catalog"},"amcheck":{"schema":"extensions"}} + {"pg_cron":{"schema":"pg_catalog"},"amcheck":{"schema":"pg_catalog"}} (1 row) -- precondition: postgres must not be a superuser, or nothing below is meaningful @@ -37,7 +32,7 @@ select rolsuper from pg_roles where rolname = 'postgres'; -- prime.sql already created amcheck; drop it so the creates below are observable drop extension if exists amcheck; --- a non-superuser can install it, and it lands in extensions rather than public +-- a non-superuser can install it, and it lands in pg_catalog rather than public set role postgres; create extension amcheck; reset role; @@ -46,7 +41,7 @@ select extowner::regrole as owner, extnamespace::regnamespace as schema where extname = 'amcheck'; owner | schema ----------------+------------ - supabase_admin | extensions + supabase_admin | pg_catalog (1 row) -- the override wins over an explicitly requested schema @@ -59,7 +54,7 @@ select extnamespace::regnamespace as schema_after_requesting_public where extname = 'amcheck'; schema_after_requesting_public -------------------------------- - extensions + pg_catalog (1 row) -- The privilege matrix. Aggregated over every function the extension owns rather @@ -110,13 +105,13 @@ select pg_get_userbyid(relowner) as notmine_owner, set role postgres; create table amcheck_test.mine(i int primary key) using heap; insert into amcheck_test.mine select generate_series(1, 100); -select extensions.bt_index_check('amcheck_test.mine_pkey'::regclass); +select pg_catalog.bt_index_check('amcheck_test.mine_pkey'::regclass); bt_index_check ---------------- (1 row) -select extensions.bt_index_check('amcheck_test.notmine_pkey'::regclass); +select pg_catalog.bt_index_check('amcheck_test.notmine_pkey'::regclass); bt_index_check ---------------- @@ -126,10 +121,10 @@ reset role; -- the API roles must not reach it. The denial is at the function level, so it -- applies to every target relation. set role anon; -select extensions.bt_index_check('auth.users_pkey'::regclass); +select pg_catalog.bt_index_check('auth.users_pkey'::regclass); ERROR: permission denied for function bt_index_check reset role; --- restore the state prime.sql created (amcheck present, in extensions) +-- restore the state prime.sql created (amcheck present, in pg_catalog) set role postgres; drop table amcheck_test.mine; reset role; diff --git a/nix/tests/expected/extensions_schema.out b/nix/tests/expected/extensions_schema.out index c2fca7f222..7d3dffa95f 100644 --- a/nix/tests/expected/extensions_schema.out +++ b/nix/tests/expected/extensions_schema.out @@ -15,9 +15,8 @@ order by e.extname; extension_name | schema_name | extension_owner --------------------+-------------+----------------- - amcheck | extensions | supabase_admin pg_stat_statements | extensions | supabase_admin pgcrypto | extensions | supabase_admin uuid-ossp | extensions | supabase_admin -(4 rows) +(3 rows) diff --git a/nix/tests/expected/z_15_ext_interface.out b/nix/tests/expected/z_15_ext_interface.out index 94de191ef4..e53086bdec 100644 --- a/nix/tests/expected/z_15_ext_interface.out +++ b/nix/tests/expected/z_15_ext_interface.out @@ -181,12 +181,12 @@ order by adminpack | pg_catalog | pg_file_unlink | text | boolean adminpack | pg_catalog | pg_file_write | text, text, boolean | bigint adminpack | pg_catalog | pg_logdir_ls | | SETOF record - amcheck | extensions | bt_index_check | index regclass | void - amcheck | extensions | bt_index_check | index regclass, heapallindexed boolean | void - amcheck | extensions | bt_index_parent_check | index regclass | void - amcheck | extensions | bt_index_parent_check | index regclass, heapallindexed boolean | void - amcheck | extensions | bt_index_parent_check | index regclass, heapallindexed boolean, rootdescend boolean | void - amcheck | extensions | verify_heapam | relation regclass, on_error_stop boolean, check_toast boolean, skip text, startblock bigint, endblock bigint, OUT blkno bigint, OUT offnum integer, OUT attnum integer, OUT msg text | SETOF record + amcheck | pg_catalog | bt_index_check | index regclass | void + amcheck | pg_catalog | bt_index_check | index regclass, heapallindexed boolean | void + amcheck | pg_catalog | bt_index_parent_check | index regclass | void + amcheck | pg_catalog | bt_index_parent_check | index regclass, heapallindexed boolean | void + amcheck | pg_catalog | bt_index_parent_check | index regclass, heapallindexed boolean, rootdescend boolean | void + amcheck | pg_catalog | verify_heapam | relation regclass, on_error_stop boolean, check_toast boolean, skip text, startblock bigint, endblock bigint, OUT blkno bigint, OUT offnum integer, OUT attnum integer, OUT msg text | SETOF record autoinc | public | autoinc | | trigger bloom | public | blhandler | internal | index_am_handler btree_gin | public | gin_btree_consistent | internal, smallint, anyelement, integer, internal, internal | boolean diff --git a/nix/tests/expected/z_15_roles.out b/nix/tests/expected/z_15_roles.out index a051ab1d15..c7aaabf361 100644 --- a/nix/tests/expected/z_15_roles.out +++ b/nix/tests/expected/z_15_roles.out @@ -51,6 +51,11 @@ where p.pronamespace::regnamespace::text = 'pg_catalog' order by object_name, grantee, privilege_type; schema | object_name | grantee | privilege_type ------------+--------------------------------+-------------------+---------------- + pg_catalog | bt_index_check | postgres | EXECUTE + pg_catalog | bt_index_check | postgres | EXECUTE + pg_catalog | bt_index_parent_check | postgres | EXECUTE + pg_catalog | bt_index_parent_check | postgres | EXECUTE + pg_catalog | bt_index_parent_check | postgres | EXECUTE pg_catalog | pg_get_backend_memory_contexts | pg_read_all_stats | EXECUTE pg_catalog | pg_get_shmem_allocations | pg_read_all_stats | EXECUTE pg_catalog | pg_ls_archive_statusdir | pg_monitor | EXECUTE @@ -62,5 +67,6 @@ order by object_name, grantee, privilege_type; pg_catalog | pg_ls_tmpdir | pg_monitor | EXECUTE pg_catalog | pg_ls_waldir | pg_monitor | EXECUTE pg_catalog | pg_reload_conf | postgres | EXECUTE -(11 rows) + pg_catalog | verify_heapam | postgres | EXECUTE +(17 rows) diff --git a/nix/tests/expected/z_17_ext_interface.out b/nix/tests/expected/z_17_ext_interface.out index 22adb257c2..d589b8a6d1 100644 --- a/nix/tests/expected/z_17_ext_interface.out +++ b/nix/tests/expected/z_17_ext_interface.out @@ -162,14 +162,14 @@ order by address_standardizer | public | parse_address | text, OUT num text, OUT street text, OUT street2 text, OUT address1 text, OUT city text, OUT state text, OUT zip text, OUT zipplus text, OUT country text | record address_standardizer | public | standardize_address | lextab text, gaztab text, rultab text, address text | stdaddr address_standardizer | public | standardize_address | lextab text, gaztab text, rultab text, micro text, macro text | stdaddr - amcheck | extensions | bt_index_check | index regclass | void - amcheck | extensions | bt_index_check | index regclass, heapallindexed boolean, checkunique boolean | void - amcheck | extensions | bt_index_check | index regclass, heapallindexed boolean | void - amcheck | extensions | bt_index_parent_check | index regclass | void - amcheck | extensions | bt_index_parent_check | index regclass, heapallindexed boolean, rootdescend boolean, checkunique boolean | void - amcheck | extensions | bt_index_parent_check | index regclass, heapallindexed boolean | void - amcheck | extensions | bt_index_parent_check | index regclass, heapallindexed boolean, rootdescend boolean | void - amcheck | extensions | verify_heapam | relation regclass, on_error_stop boolean, check_toast boolean, skip text, startblock bigint, endblock bigint, OUT blkno bigint, OUT offnum integer, OUT attnum integer, OUT msg text | SETOF record + amcheck | pg_catalog | bt_index_check | index regclass | void + amcheck | pg_catalog | bt_index_check | index regclass, heapallindexed boolean, checkunique boolean | void + amcheck | pg_catalog | bt_index_check | index regclass, heapallindexed boolean | void + amcheck | pg_catalog | bt_index_parent_check | index regclass | void + amcheck | pg_catalog | bt_index_parent_check | index regclass, heapallindexed boolean, rootdescend boolean, checkunique boolean | void + amcheck | pg_catalog | bt_index_parent_check | index regclass, heapallindexed boolean | void + amcheck | pg_catalog | bt_index_parent_check | index regclass, heapallindexed boolean, rootdescend boolean | void + amcheck | pg_catalog | verify_heapam | relation regclass, on_error_stop boolean, check_toast boolean, skip text, startblock bigint, endblock bigint, OUT blkno bigint, OUT offnum integer, OUT attnum integer, OUT msg text | SETOF record autoinc | public | autoinc | | trigger bloom | public | blhandler | internal | index_am_handler btree_gin | public | gin_btree_consistent | internal, smallint, anyelement, integer, internal, internal | boolean diff --git a/nix/tests/expected/z_17_roles.out b/nix/tests/expected/z_17_roles.out index e7ee480708..b966f8f9d5 100644 --- a/nix/tests/expected/z_17_roles.out +++ b/nix/tests/expected/z_17_roles.out @@ -184,6 +184,13 @@ where p.pronamespace::regnamespace::text = 'pg_catalog' order by object_name, grantee, privilege_type; schema | object_name | grantee | privilege_type ------------+--------------------------------+-------------------+---------------- + pg_catalog | bt_index_check | postgres | EXECUTE + pg_catalog | bt_index_check | postgres | EXECUTE + pg_catalog | bt_index_check | postgres | EXECUTE + pg_catalog | bt_index_parent_check | postgres | EXECUTE + pg_catalog | bt_index_parent_check | postgres | EXECUTE + pg_catalog | bt_index_parent_check | postgres | EXECUTE + pg_catalog | bt_index_parent_check | postgres | EXECUTE pg_catalog | pg_current_logfile | pg_monitor | EXECUTE pg_catalog | pg_current_logfile | pg_monitor | EXECUTE pg_catalog | pg_get_backend_memory_contexts | pg_read_all_stats | EXECUTE @@ -197,5 +204,6 @@ order by object_name, grantee, privilege_type; pg_catalog | pg_ls_tmpdir | pg_monitor | EXECUTE pg_catalog | pg_ls_waldir | pg_monitor | EXECUTE pg_catalog | pg_reload_conf | postgres | EXECUTE -(13 rows) + pg_catalog | verify_heapam | postgres | EXECUTE +(21 rows) diff --git a/nix/tests/expected/z_orioledb-17_ext_interface.out b/nix/tests/expected/z_orioledb-17_ext_interface.out index 30f09b5ede..0482d0b86b 100644 --- a/nix/tests/expected/z_orioledb-17_ext_interface.out +++ b/nix/tests/expected/z_orioledb-17_ext_interface.out @@ -163,14 +163,14 @@ order by address_standardizer | public | parse_address | text, OUT num text, OUT street text, OUT street2 text, OUT address1 text, OUT city text, OUT state text, OUT zip text, OUT zipplus text, OUT country text | record address_standardizer | public | standardize_address | lextab text, gaztab text, rultab text, address text | stdaddr address_standardizer | public | standardize_address | lextab text, gaztab text, rultab text, micro text, macro text | stdaddr - amcheck | extensions | bt_index_check | index regclass | void - amcheck | extensions | bt_index_check | index regclass, heapallindexed boolean, checkunique boolean | void - amcheck | extensions | bt_index_check | index regclass, heapallindexed boolean | void - amcheck | extensions | bt_index_parent_check | index regclass | void - amcheck | extensions | bt_index_parent_check | index regclass, heapallindexed boolean, rootdescend boolean, checkunique boolean | void - amcheck | extensions | bt_index_parent_check | index regclass, heapallindexed boolean | void - amcheck | extensions | bt_index_parent_check | index regclass, heapallindexed boolean, rootdescend boolean | void - amcheck | extensions | verify_heapam | relation regclass, on_error_stop boolean, check_toast boolean, skip text, startblock bigint, endblock bigint, OUT blkno bigint, OUT offnum integer, OUT attnum integer, OUT msg text | SETOF record + amcheck | pg_catalog | bt_index_check | index regclass | void + amcheck | pg_catalog | bt_index_check | index regclass, heapallindexed boolean, checkunique boolean | void + amcheck | pg_catalog | bt_index_check | index regclass, heapallindexed boolean | void + amcheck | pg_catalog | bt_index_parent_check | index regclass | void + amcheck | pg_catalog | bt_index_parent_check | index regclass, heapallindexed boolean, rootdescend boolean, checkunique boolean | void + amcheck | pg_catalog | bt_index_parent_check | index regclass, heapallindexed boolean | void + amcheck | pg_catalog | bt_index_parent_check | index regclass, heapallindexed boolean, rootdescend boolean | void + amcheck | pg_catalog | verify_heapam | relation regclass, on_error_stop boolean, check_toast boolean, skip text, startblock bigint, endblock bigint, OUT blkno bigint, OUT offnum integer, OUT attnum integer, OUT msg text | SETOF record autoinc | public | autoinc | | trigger bloom | public | blhandler | internal | index_am_handler btree_gin | public | gin_btree_consistent | internal, smallint, anyelement, integer, internal, internal | boolean diff --git a/nix/tests/expected/z_orioledb-17_extensions_schema.out b/nix/tests/expected/z_orioledb-17_extensions_schema.out index 4456d9ace2..2eafa8119c 100644 --- a/nix/tests/expected/z_orioledb-17_extensions_schema.out +++ b/nix/tests/expected/z_orioledb-17_extensions_schema.out @@ -15,10 +15,9 @@ order by e.extname; extension_name | schema_name | extension_owner --------------------+-------------+----------------- - amcheck | extensions | supabase_admin orioledb | extensions | supabase_admin pg_stat_statements | extensions | supabase_admin pgcrypto | extensions | supabase_admin uuid-ossp | extensions | supabase_admin -(5 rows) +(4 rows)