Skip to content

SEGFAULT in privilege-check path: pgTAP throws_ok on REVOKEd SECURITY DEFINER (PG 17.6 aarch64) #2124

@Curtelisor

Description

@Curtelisor

Postgres SEGFAULT on pgTAP throws_ok / throws_like runtime-call against REVOKEd SECURITY DEFINER function

Environment

  • Image: public.ecr.aws/supabase/postgres:17.6.1.106
  • Postgres: 17.6 on aarch64-unknown-linux-gnu, compiled by gcc (GCC) 15.2.0, 64-bit
  • pgTAP: 1.3.3
  • Host: macOS 14 (Apple Silicon, Docker Desktop)
  • Reproducible: yes — observed 3+ separate times in pgTAP test suites; deterministic with the repro below.

Summary

When a SECURITY DEFINER function has had EXECUTE revoked from a role, calling it via pgTAP throws_ok() / throws_like() while that role is active causes a Postgres backend SEGFAULT (signal 11). The whole DB enters recovery mode.

The same call outside throws_ok (i.e. plain SELECT public.fn() as the same role) raises 42501 insufficient_privilege cleanly — only the dynamic-EXECUTE path inside throws_ok crashes.

Symptom

psql:repro.sql:NN: server closed the connection unexpectedly
        This probably means the server terminated abnormally
        before or while processing the request.
psql:repro.sql:NN: error: connection to server was lost
psql: error: connection to server at "db" failed:
        FATAL:  the database system is in recovery mode

Minimal repro

See attached upstream-segfault-repro.sql. To run:

docker exec -i <supabase_db_container> psql -U postgres -d postgres < upstream-segfault-repro.sql

Trigger conditions (all required):

  1. Function is SECURITY DEFINER.
  2. REVOKE … FROM <role> (or from PUBLIC such that the role inherits no grant) was applied for that function and role.
  3. Function is invoked via throws_ok / throws_like (i.e. through the dynamic-EXECUTE path) while the session is SET LOCAL ROLE <role>.

Expected behavior

throws_ok should observe 42501 insufficient_privilege and report the test as ok (the same SQLSTATE the equivalent direct call raises).

Actual behavior

Backend SEGFAULTs in the privilege-check path; postmaster restarts; connection is killed; subsequent connections see FATAL: the database system is in recovery mode until recovery completes.

Suspected location

The crash appears to happen in the EXECUTE privilege check for SECURITY DEFINER functions when invoked through SPI / dynamic SQL. The non-throws_ok runtime path (plain SELECT) handles the same condition cleanly with ERROR: permission denied for function ….

Workaround (used downstream)

Replace the runtime call inside throws_ok with a pg_proc.proacl metadata check:

SELECT is(
  (SELECT
    CASE WHEN proacl IS NULL THEN -1
         ELSE (SELECT count(*)::int FROM aclexplode(proacl) ax
               WHERE ax.grantee::regrole::text IN ('anon', 'public')
                 AND ax.privilege_type = 'EXECUTE') END
   FROM pg_proc
   WHERE proname = 'demo_secdef'
     AND pronamespace = 'public'::regnamespace),
  0,
  'demo_secdef has no EXECUTE grant for anon (REVOKED)'
);

Semantically equivalent — a missing EXECUTE grant is exactly what would raise 42501 at runtime — without invoking the crashing path.

Why this matters

Defense-in-depth REVOKE patterns on SECURITY DEFINER functions are common in Supabase apps (anon REVOKE for service-role-only RPCs, etc.). pgTAP tests of those REVOKE contracts are blocked by this crash. Today the only options are: (a) skip the test, (b) the metadata workaround above, (c) avoid SECURITY DEFINER entirely. None are great for a CI-friendly default.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions