restrict_nonsystem_relation_kind is a security patch that happens to be spelled like a configuration parameter. You will almost never set it. pg_dump sets it, on its own connection, as the last thing it does before opening its transaction, and the entire reason the parameter exists is so that it can. The manual gives it three sentences and never says the word pg_dump in any of them, which is a shame, because without that word the entry reads like a knob with no purpose.

The default is the empty string, meaning nothing is restricted, and the context is user, so any session can set it, or unset it, for itself. The value is a comma-separated list drawn from exactly two key words, view and foreign-table (case-insensitive, but exact: views is rejected with “Unrecognized key word”). It is not in postgresql.conf.sample, and it did not arrive in a major release. It arrived in the minor releases of August 8, 2024: 16.4, 15.8, 14.13, 13.16 and 12.20, plus 17 from beta 3 onward. Every supported major has carried it since.

With view in the list, the rewriter refuses to expand any view whose OID is 16384 or higher; that is FirstNormalObjectId, the first OID handed out after initdb finishes. “System” here means “born during initdb”, not “lives in pg_catalog”. pg_views and the information_schema views are fine. The pg_stat_statements view is not, because the extension created it after the fact, and neither is a view you smuggle into pg_catalog with allow_system_table_mods; both fail with access to non-system view "..." is restricted. The check sits at the moment a view is replaced by its defining query, so everything that passes through the rewriter is caught: SELECT, EXPLAIN, INSERT and UPDATE through an auto-updatable view, COPY (SELECT ...) TO, CREATE MATERIALIZED VIEW ... AS over the view, a PL/pgSQL function that reads it. Everything that does not pass through the rewriter is untouched: \d, pg_get_viewdef(), DROP VIEW, and even CREATE VIEW v2 AS SELECT * FROM v, which analyzes its query without expanding it (selecting from v2 afterwards fails like everything else). Materialized views carry an ON SELECT rule like any other view, but the rewriter never fires it for relkind m; they are read as tables, so they are not views for this purpose and read normally.

With foreign-table in the list, the planner refuses any foreign table it meets, and the function that fetches a foreign data wrapper’s handler refuses to be called at all. The error is access to non-system foreign table is restricted, with no name, because there are no system foreign tables to tell it apart from. SELECT, DML, TRUNCATE and IMPORT FOREIGN SCHEMA all fail. (ANALYZE on a foreign table slips through if the session has already touched that table, because the handler is cached in the relcache and the cached path has no check. pg_dump never analyzes anything, so this is a curiosity rather than a hole, but it tells you the parameter was built for one caller, not as a sandbox.)

That caller is pg_dump, and the reason is CVE-2024-7348, reported by Noah Misch and fixed by Masahiko Sawada. pg_dump works in two passes. It reads the catalogs to learn what every relation is, and later, sometimes much later, it issues SQL against those relations by name. Between the two, a user who can create objects in a schema can drop their own sequence and create a view with the same name, and the view’s definition can call any function they like. When pg_dump arrives with SELECT last_value, is_called FROM thatname, the server expands the view and runs the function as pg_dump’s role, which is usually a superuser. Winning the race is not even hard: the attacker does the drop-and-create inside a transaction and leaves it open, pg_dump’s query blocks on the lock, the attacker commits, and the name now resolves to the view. The same swap works with a foreign table, which hands the query to whatever wrapper sits behind it.

Three pg_dump code paths issue SELECTs by name and were exposed: sequence state (on servers before 18), tables registered with pg_extension_config_dump() and a WHERE filter, which are dumped as COPY (SELECT ... FROM name WHERE ...), and foreign-table data under --include-foreign-data. Ordinary tables in an ordinary dump were never the problem; they are dumped with COPY name TO stdout, and COPY refuses views and foreign tables outright (“cannot copy from view”). The exception is --inserts and its relatives, which read every table through a cursor over SELECT ... FROM ONLY name, and FROM ONLY expands a view just as readily as FROM does; that path is covered by the same setting. On 18 and later the sequence path is gone as well: pg_dump collects every sequence’s last_value and is_called in one catalog query through pg_get_sequence_data(seqrelid), by OID, and nothing an attacker creates can have the old OID. The parameter still covers the other two paths, and every pre-18 server.

Rather than audit each query for a race that is inherent in two-pass dumping, the fix makes the server refuse the swapped-in object. Here is the defense doing its job on 18.6, in one session with no race, so you can see the error rather than take my word for it:

1postgres=# set restrict_nonsystem_relation_kind = 'view, foreign-table';
2SET
3postgres=# select last_value, is_called from seq_swap;
4 last_value | is_called
5------------+-----------
6 1 | f
7(1 row)
8
9postgres=# drop sequence seq_swap;
10DROP SEQUENCE
11postgres=# create view seq_swap as select 1::bigint as last_value, true as is_called;
12CREATE VIEW
13postgres=# select last_value, is_called from seq_swap;
14ERROR: access to non-system view "seq_swap" is restricted

What pg_dump actually sends

Turn on log_statement = 'all' and dump a database, and the connection setup ends with this, right before BEGIN:

1SELECT set_config(name, 'view, foreign-table', false)
2 FROM pg_settings
3 WHERE name = 'restrict_nonsystem_relation_kind'

The shape is deliberate. Because the parameter arrived in minor releases, deciding whether a server has it from the version number would mean a different threshold for every branch, so pg_dump asks pg_settings instead; on a server that lacks it the query matches no rows, sets nothing, and succeeds. It runs once per connection, including each worker of a parallel dump, after the other settings that keep a dump deterministic: extra_float_digits to 3, synchronize_seqscans off, statement_timeout, lock_timeout, idle_in_transaction_session_timeout and (on 17 and later) transaction_timeout all to 0, and row_security off. With --include-foreign-data, pg_dump lowers the setting to plain view immediately before each foreign table’s COPY (SELECT ...) and raises it again immediately after, which is as narrow a window as it can make; the manual’s note that the named server must have a trusted owner is the residual risk it cannot close. pg_dumpall never sets it on its own connection, because that connection only reads global catalogs; the per-database pg_dump children it spawns do. pg_upgrade rides on pg_dump and inherits it.

The release notes make a point that is easy to skim past: the protection needs both ends. A patched server does nothing on its own, and a patched pg_dump against an unpatched server sends a query that sets nothing. The combination to worry about is not an old server but an old client, the backup host whose pg_dump was installed when the box was built and has not been touched since (a 16.3 pg_dump against a 16.4 server, say; an old client cannot dump a newer major at all, so the gap only opens within a major or against an older one). That one you can close yourself, because the context is user: ALTER ROLE backup_role SET restrict_nonsystem_relation_kind = 'view, foreign-table', or PGOPTIONS='-c restrict_nonsystem_relation_kind=view,foreign-table' in the backup job’s environment. A current pg_dump still sends its own set_config and still lowers the value around foreign-table data, so the role default costs it nothing; an old pg_dump is covered on the paths that matter and will fail loudly, rather than dangerously, if it is asked for --include-foreign-data. Do it on a dedicated role. A role default of view breaks every ordinary query against every ordinary view that role runs, and the first application that borrows the backup credentials will discover that at 2 a.m.

What the parameter is not is an access control. The context is user, so a hostile session resets it in one statement; it protects a cooperating client from the contents of the database, not the database from the client. If you want to stop a role from reading views, revoke privileges. If you want to stop a role from planting objects that a superuser’s tooling will later trip over, that is CREATE on the schema, and it is the actual lesson of the CVE: anyone who can create relations in your database can arrange for a superuser’s tools to run their code, and the only durable defense is being stingy about who can create what.

So: leave it alone and let pg_dump set it. Pin it on the backup role if you cannot vouch for the age of every pg_dump binary that touches your servers. And apply your minor releases; this is one of the quieter reasons why.