Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -400,6 +400,17 @@ true until the next version shipped.
Reported by @OffgridwithJD.

### Fixed
- **A NOLOGIN role can run `parallel_export_parquet`, `parallel_copy` and the
parallel flush on PostgreSQL 15 and 16.** After `SET ROLE` to a role without
LOGIN, the export failed `worker 0 failed: worker exited without reporting a
result`, `parallel_copy` failed `coordinator exited without reporting a result`,
and the parallel flush encoded every column serially. Each background worker
connected as the calling role, which these majors refuse for a role that cannot
log in. A worker now connects as the session's authenticated user and then takes
the caller's user id and security context, as PostgreSQL's own parallel workers
do, so triggers, privilege checks and `current_user` inside it still see the
caller. PostgreSQL 17 and later already worked (#1585).
`test/nologin_workers.sh` and `test_nologin_workers.py`.
- **In the upgrade window, a refusal for a missing catalog table names `ALTER
EXTENSION UPDATE`.** After the new library is installed and the server restarted,
but before `ALTER EXTENSION pgcolumnar UPDATE`, `DROP TABLE`, `TRUNCATE` and
Expand Down
54 changes: 54 additions & 0 deletions src/columnar.h
Original file line number Diff line number Diff line change
Expand Up @@ -1273,4 +1273,58 @@ PgColumnarVarSizeAnyUnalignedBounded(const char *p, const char *end)
return len;
}

/*
* Connect a background worker that does a caller's work, and act as that caller
* (#1585, #1612). The leader records the caller as userid, with its superuser flag
* and security context (GetUserIdAndSecContext), and GetAuthenticatedUserId() as
* authid.
*
* THE WORKER'S SESSION USER IS THE CALLER on every major, so a RESET ROLE inside the
* worker (a copy trigger can run one) lands on the caller and not on some other user.
* On 17 and later the worker connects as the caller, with the login check bypassed,
* as it always did. Before 17 there is no such bypass and a NOLOGIN caller cannot be
* connected as, so the worker connects as the authenticated user, which logged in, and
* then makes the caller its session user. Connecting as the authenticated user and
* stopping there made it the session user: a trigger's RESET ROLE then became that
* user, the copy PREPAREd as it, and the leader could not finish the transaction
* (#1612 review).
*
* THE SECURITY CONTEXT TRAVELS TOO, as core's ParallelWorkerMain carries it, so a call
* made inside a SECURITY DEFINER function or a security-restricted operation keeps its
* restrictions in the worker: without it, a trigger there could switch to the session
* user (#1612 review). It is taken inside the transaction; see
* columnar_worker_take_context below.
*/
#include "miscadmin.h"
#include "postmaster/bgworker.h"

static inline void
columnar_worker_connect_as(Oid dbid, Oid authid, Oid userid, bool userIsSuperuser)
{
#if PG_VERSION_NUM >= 170000
(void) authid;
(void) userIsSuperuser;
BackgroundWorkerInitializeConnectionByOid(dbid, userid,
BGWORKER_BYPASS_ALLOWCONN |
BGWORKER_BYPASS_ROLELOGINCHECK);
#else
BackgroundWorkerInitializeConnectionByOid(dbid, authid, BGWORKER_BYPASS_ALLOWCONN);
SetSessionAuthorization(userid, userIsSuperuser); /* the current user too */
#endif
}

/*
* Take the caller's security context, INSIDE the worker's transaction. A top-level
* transaction must start with none -- StartTransaction asserts it, which is why core's
* ParallelWorkerMain sets it after its transaction begins -- and commit or abort
* restores the transaction's starting state. Set before the first transaction, an
* assert-enabled build aborted the coordinator (#1612 review). Called by the workers
* that run user code: the copy loader (triggers) and the export worker.
*/
static inline void
columnar_worker_take_context(Oid userid, int sec_context)
{
SetUserIdAndSecContext(userid, sec_context);
}

#endif /* PGCOLUMNAR_H */
46 changes: 34 additions & 12 deletions src/columnar_parallel_copy.c
Original file line number Diff line number Diff line change
Expand Up @@ -388,7 +388,10 @@ typedef struct PcopyWorkerSlot
typedef struct PcopyHeader
{
Oid dbid;
Oid roleid;
Oid roleid; /* the caller's user id, with secContext */
int secContext;
Oid authid; /* the authenticated user, which workers connect as below 17 (#1585) */
bool roleIsSuperuser; /* roleid's superuser flag, for the worker's session user */
Oid relid; /* target relation (partitioned parent, or a single columnar table) */
int nworkers;
bool single_table; /* target is one columnar table (not partitioned):
Expand Down Expand Up @@ -816,7 +819,6 @@ pgcolumnar_parallel_copy_worker(Datum main_arg)
PcopyWorkerSlot *slots;
PcopyWorkerSlot *me;
int widx;
uint32 conn_flags = BGWORKER_BYPASS_ALLOWCONN;

memcpy(&widx, MyBgworkerEntry->bgw_extra, sizeof(int));

Expand All @@ -837,10 +839,8 @@ pgcolumnar_parallel_copy_worker(Datum main_arg)
slots = (PcopyWorkerSlot *) shm_toc_lookup(toc, PCOPY_KEY_WORKERS, false);
me = &slots[widx];

#if PG_VERSION_NUM >= 170000
conn_flags |= BGWORKER_BYPASS_ROLELOGINCHECK;
#endif
BackgroundWorkerInitializeConnectionByOid(hdr->dbid, hdr->roleid, conn_flags);
columnar_worker_connect_as(hdr->dbid, hdr->authid, hdr->roleid,
hdr->roleIsSuperuser);

/*
* Single-table load: this loader writes the one shared storage concurrently
Expand Down Expand Up @@ -870,6 +870,8 @@ pgcolumnar_parallel_copy_worker(Datum main_arg)
StartTransactionCommand();
BeginTransactionBlock();
CommitTransactionCommand(); /* TBLOCK_BEGIN -> in progress */
/* the caller's security context, now that a transaction is open (#1612) */
columnar_worker_take_context(hdr->roleid, hdr->secContext);

fd = pcopy_open_regular_file(hdr->filename, NULL);
if (lseek(fd, (off_t) me->start_off, SEEK_SET) < 0)
Expand Down Expand Up @@ -951,6 +953,27 @@ pgcolumnar_parallel_copy_worker(Datum main_arg)
* loader holds no transaction, so writing the result slot is a plain
* shared-memory store.
*/
/*
* PREPARE only as the caller (#1612 review). A trigger can change role
* inside this transaction (SET ROLE to a role the caller belongs to, or a
* transaction-local set_config('role')), and a transaction prepared as
* another user is one the coordinator, running as the caller, cannot
* finish: it would stay prepared, holding its locks, until someone ran
* ROLLBACK PREPARED. Refused here it aborts like any other error, and the
* coordinator rolls the load back.
*/
{
Oid nowUser;
int nowContext;

GetUserIdAndSecContext(&nowUser, &nowContext);
if (nowUser != hdr->roleid || nowContext != hdr->secContext)
ereport(ERROR,
(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
errmsg("pgcolumnar parallel_copy worker is no longer running as the role that called parallel_copy"),
errdetail("A trigger changed the role during the load, so its transaction cannot be prepared for the caller to finish.")));
}

if (!PrepareTransactionBlock(me->gid))
ereport(ERROR,
(errcode(ERRCODE_INTERNAL_ERROR),
Expand Down Expand Up @@ -1142,7 +1165,6 @@ pgcolumnar_parallel_copy_coordinator(Datum main_arg)
PcopyHeader *hdr;
PcopyWorkerSlot *slots;
int nworkers;
uint32 conn_flags = BGWORKER_BYPASS_ALLOWCONN;
BackgroundWorker bw;
BackgroundWorkerHandle **handles;
int i;
Expand All @@ -1165,10 +1187,8 @@ pgcolumnar_parallel_copy_coordinator(Datum main_arg)
slots = (PcopyWorkerSlot *) shm_toc_lookup(toc, PCOPY_KEY_WORKERS, false);
nworkers = hdr->nworkers;

#if PG_VERSION_NUM >= 170000
conn_flags |= BGWORKER_BYPASS_ROLELOGINCHECK;
#endif
BackgroundWorkerInitializeConnectionByOid(hdr->dbid, hdr->roleid, conn_flags);
columnar_worker_connect_as(hdr->dbid, hdr->authid, hdr->roleid,
hdr->roleIsSuperuser);

/*
* Single-table load: pre-create and COMMIT the storage catalog row before any
Expand Down Expand Up @@ -1623,7 +1643,9 @@ pgcolumnar_parallel_copy(PG_FUNCTION_ARGS)
dsmh = dsm_segment_handle(seg);

hdr->dbid = MyDatabaseId;
hdr->roleid = GetUserId();
GetUserIdAndSecContext(&hdr->roleid, &hdr->secContext);
hdr->authid = GetAuthenticatedUserId();
hdr->roleIsSuperuser = superuser_arg(hdr->roleid);
hdr->relid = relid;
hdr->nworkers = workers;
hdr->single_table = single_table;
Expand Down
18 changes: 11 additions & 7 deletions src/columnar_parallel_export.c
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,10 @@ typedef struct PexportWorkerSlot
typedef struct PexportHeader
{
Oid dbid;
Oid roleid;
Oid roleid; /* the caller's user id, with secContext */
int secContext;
Oid authid; /* the authenticated user, which workers connect as below 17 (#1585) */
bool roleIsSuperuser; /* roleid's superuser flag, for the worker's session user */
Oid relid; /* single columnar table, or partitioned parent */
uint64 storageId; /* single-table storage id (0 when partitioned) */
int nworkers;
Expand Down Expand Up @@ -452,7 +455,6 @@ pgcolumnar_parallel_export_worker(Datum main_arg)
Oid *leaves;
PexportWorkerSlot *me;
int widx;
uint32 conn_flags = BGWORKER_BYPASS_ALLOWCONN;
Snapshot snap;

memcpy(&widx, MyBgworkerEntry->bgw_extra, sizeof(int));
Expand All @@ -475,17 +477,17 @@ pgcolumnar_parallel_export_worker(Datum main_arg)
leaves = (Oid *) shm_toc_lookup(toc, PEXPORT_KEY_LEAVES, false);
me = &slots[widx];

#if PG_VERSION_NUM >= 170000
conn_flags |= BGWORKER_BYPASS_ROLELOGINCHECK;
#endif
BackgroundWorkerInitializeConnectionByOid(hdr->dbid, hdr->roleid, conn_flags);
columnar_worker_connect_as(hdr->dbid, hdr->authid, hdr->roleid,
hdr->roleIsSuperuser);

/*
* Adopt the dispatcher's exact snapshot. ImportSnapshot requires a
* repeatable-read (or serializable) transaction with no snapshot taken yet,
* exactly as SET TRANSACTION SNAPSHOT does.
*/
StartTransactionCommand();
/* the caller's security context, now that a transaction is open (#1612) */
columnar_worker_take_context(hdr->roleid, hdr->secContext);
XactIsoLevel = XACT_REPEATABLE_READ;
ImportSnapshot(hdr->snapname);
snap = GetTransactionSnapshot();
Expand Down Expand Up @@ -759,7 +761,9 @@ pgcolumnar_parallel_export_parquet(PG_FUNCTION_ARGS)
dsmh = dsm_segment_handle(seg);

hdr->dbid = MyDatabaseId;
hdr->roleid = GetUserId();
GetUserIdAndSecContext(&hdr->roleid, &hdr->secContext);
hdr->authid = GetAuthenticatedUserId();
hdr->roleIsSuperuser = superuser_arg(hdr->roleid);
hdr->relid = relid;
hdr->storageId = storageId;
hdr->nworkers = workers;
Expand Down
16 changes: 9 additions & 7 deletions src/columnar_write_state.c
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,10 @@ typedef struct PflushWorkerSlot
typedef struct PflushHeader
{
Oid dbid;
Oid roleid;
Oid roleid; /* the caller's user id, with secContext */
int secContext;
Oid authid; /* the authenticated user, which workers connect as below 17 (#1585) */
bool roleIsSuperuser; /* roleid's superuser flag, for the worker's session user */
Oid relid;
uint64 storageId;
uint64 groupNumber;
Expand Down Expand Up @@ -2440,7 +2443,6 @@ pgcolumnar_parallel_flush_worker(Datum main_arg)
pg_atomic_uint32 *claim;
PflushWorkerSlot *me;
int widx;
uint32 conn_flags = BGWORKER_BYPASS_ALLOWCONN;

memcpy(&widx, MyBgworkerEntry->bgw_extra, sizeof(int));

Expand All @@ -2465,10 +2467,8 @@ pgcolumnar_parallel_flush_worker(Datum main_arg)
claim = (pg_atomic_uint32 *) shm_toc_lookup(toc, PFLUSH_KEY_CLAIM, false);
me = &slots[widx];

#if PG_VERSION_NUM >= 170000
conn_flags |= BGWORKER_BYPASS_ROLELOGINCHECK;
#endif
BackgroundWorkerInitializeConnectionByOid(hdr->dbid, hdr->roleid, conn_flags);
columnar_worker_connect_as(hdr->dbid, hdr->authid, hdr->roleid,
hdr->roleIsSuperuser);

/*
* A fresh snapshot for catalog/tupdesc reads only. A plain
Expand Down Expand Up @@ -2770,7 +2770,9 @@ flush_columns_parallel(PgColumnarWriteState *writeState, uint64 groupNumber,
pg_atomic_init_u32(claim, 0);

hdr->dbid = MyDatabaseId;
hdr->roleid = GetUserId();
GetUserIdAndSecContext(&hdr->roleid, &hdr->secContext);
hdr->authid = GetAuthenticatedUserId();
hdr->roleIsSuperuser = superuser_arg(hdr->roleid);
hdr->relid = writeState->relid;
hdr->storageId = writeState->storageId;
hdr->groupNumber = groupNumber;
Expand Down
Loading
Loading