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):
- Function is
SECURITY DEFINER.
REVOKE … FROM <role> (or from PUBLIC such that the role inherits no grant) was applied for that function and role.
- 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.
Postgres SEGFAULT on pgTAP
throws_ok/throws_likeruntime-call against REVOKEd SECURITY DEFINER functionEnvironment
public.ecr.aws/supabase/postgres:17.6.1.10617.6 on aarch64-unknown-linux-gnu, compiled by gcc (GCC) 15.2.0, 64-bit1.3.3Summary
When a
SECURITY DEFINERfunction has hadEXECUTErevoked from a role, calling it via pgTAPthrows_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. plainSELECT public.fn()as the same role) raises42501 insufficient_privilegecleanly — only the dynamic-EXECUTE path insidethrows_okcrashes.Symptom
Minimal repro
See attached
upstream-segfault-repro.sql. To run:Trigger conditions (all required):
SECURITY DEFINER.REVOKE … FROM <role>(or fromPUBLICsuch that the role inherits no grant) was applied for that function and role.throws_ok/throws_like(i.e. through the dynamic-EXECUTE path) while the session isSET LOCAL ROLE <role>.Expected behavior
throws_okshould observe42501 insufficient_privilegeand report the test asok(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 modeuntil recovery completes.Suspected location
The crash appears to happen in the
EXECUTEprivilege check forSECURITY DEFINERfunctions when invoked through SPI / dynamic SQL. The non-throws_okruntime path (plainSELECT) handles the same condition cleanly withERROR: permission denied for function ….Workaround (used downstream)
Replace the runtime call inside
throws_okwith apg_proc.proaclmetadata check:Semantically equivalent — a missing EXECUTE grant is exactly what would raise
42501at 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.