Skip to content

aclchk does not detoast a compressed ACL: any privilege check on a function with ≥113 grantees fails #110

Description

@AntonOfTheWoods

catalog/aclchk's with_acl_datum reads a stored aclitem[] in place and raises on any varlena that is not plain, so once the toaster stores a catalog ACL compressed, every subsequent privilege check, GRANT and REVOKE against that object fails.

Version: pgrust 0.2 (PostgreSQL 18.3 compatible), commit d13d781fb9 (a spike branch on top of the public 0.2 tree; the affected code is unchanged from upstream).
Targets: reproduced on wasm32-wasip1-threads (64-bit Datum, memory store) and on a native x86_64-unknown-linux-gnu --host-pipes build of the same commit. Not target-specific.

Reproduction:

DO $$
BEGIN
  FOR i IN 1..113 LOOP
    EXECUTE format('CREATE ROLE pgrust_repro_g%s', i);
  END LOOP;
END;
$$;

CREATE FUNCTION public.pgrust_repro_f() RETURNS int LANGUAGE sql AS 'SELECT 1';

DO $$
BEGIN
  EXECUTE 'GRANT EXECUTE ON FUNCTION public.pgrust_repro_f() TO '
    || (SELECT string_agg(format('pgrust_repro_g%s', i), ', ') FROM generate_series(1, 113) AS i);
END;
$$;

SELECT has_function_privilege('pgrust_repro_g1', 'public.pgrust_repro_f()', 'EXECUTE') AS may_execute;

Actual:

ERROR:  aclchk: compressed/external ACL varlena — detoast gap
thread 'pg:backend:1031' panicked at crates/backend/catalog/aclchk/src/lib.rs:92:13:
aclchk: compressed/external ACL varlena — detoast gap

Expected (PostgreSQL 18.3, and PGlite 0.5.5 which is PostgreSQL 18.3 compiled to wasm): one row, t.

What is required to hit it

A proacl the toaster did not leave plain, and then any aclchk read of it. 113 grantees is the measured boundary on an 8 kB-page build: 115 aclitems (the roles plus the owner plus PUBLIC) = 1864 bytes, which pushes the pg_proc tuple past TOAST_TUPLE_THRESHOLD and is stored compressed at 402 bytes. 112 grantees (114 items, 1848 bytes) still fits plain and answers normally. PGlite reports the same pg_column_size(proacl) at every count, so this is not a divergence in the toaster — only in the reader.

Grantee count is just the shortest way to write the condition. A four-item ACL in a pg_proc tuple with a ~4.5 kB prosrc is compressed the same way: there, a first REVOKE ALL … FROM PUBLIC succeeds (the ACL is still plain, 85 bytes) and rewrites proacl as a 38-byte compressed value, and the next ACL read fails.

Readers affected: has_function_privilege and REVOKE both fail. aclexplode(p.proacl) is fine — it goes through utils/adt/acl, which detoasts — so the gap is specifically the in-place reader in aclchk.

Where

crates/backend/catalog/aclchk/src/lib.rs:80 with_acl_datum; the panic! is at :92:

if varatt::varatt_is_1b_e(p) || (!varatt::varatt_is_1b(p) && !varatt::varatt_is_4b_u(p)) {
    // pg_class/pg_attribute have no toast tables; only inline
    // compression can appear here.
    panic!("aclchk: compressed/external ACL varlena — detoast gap");
}

The comment names the assumption: pg_proc does have a toast table (pg_toast_1255), and inline compression alone reaches the panic! in any case. C's DatumGetAclP (utils/acl.h) is PG_DETOAST_DATUM, which handles both before any item is read.

crates/backend/catalog/aclchk/src/grant.rs:1862 carries a guard of the same shape over pg_parameter_acl.parname (a text field, not an ACL). Not reproduced here, but it may be the same omission.

Impact seen in the wild. A deny-by-default install that revokes PUBLIC and then converges the remaining grantees — two ACL reads on the same function, in one migration — fails on the second.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions