From 20b4fbe435d31c4e784ce56c887a8d5c365d8ea5 Mon Sep 17 00:00:00 2001 From: Jacob Champion Date: Wed, 26 Mar 2025 10:55:28 -0700 Subject: [PATCH v2] WIP: split Device Authorization flow into dlopen'd module See notes on mailing list. Co-authored-by: Daniel Gustafsson --- meson.build | 12 +- src/interfaces/Makefile | 9 + src/interfaces/libpq-oauth/Makefile | 56 ++++++ src/interfaces/libpq-oauth/README | 18 ++ src/interfaces/libpq-oauth/exports.txt | 4 + src/interfaces/libpq-oauth/meson.build | 32 +++ src/interfaces/libpq-oauth/nls.mk | 15 ++ .../oauth-curl.c} | 9 +- src/interfaces/libpq-oauth/oauth-curl.h | 23 +++ src/interfaces/libpq-oauth/oauth-utils.c | 190 ++++++++++++++++++ src/interfaces/libpq-oauth/oauth-utils.h | 22 ++ src/interfaces/libpq-oauth/po/LINGUAS | 0 src/interfaces/libpq-oauth/po/meson.build | 3 + src/interfaces/libpq/Makefile | 4 - src/interfaces/libpq/exports.txt | 1 + src/interfaces/libpq/fe-auth-oauth.c | 52 ++++- src/interfaces/libpq/fe-auth-oauth.h | 4 +- src/interfaces/libpq/meson.build | 4 - 18 files changed, 431 insertions(+), 27 deletions(-) create mode 100644 src/interfaces/libpq-oauth/Makefile create mode 100644 src/interfaces/libpq-oauth/README create mode 100644 src/interfaces/libpq-oauth/exports.txt create mode 100644 src/interfaces/libpq-oauth/meson.build create mode 100644 src/interfaces/libpq-oauth/nls.mk rename src/interfaces/{libpq/fe-auth-oauth-curl.c => libpq-oauth/oauth-curl.c} (99%) create mode 100644 src/interfaces/libpq-oauth/oauth-curl.h create mode 100644 src/interfaces/libpq-oauth/oauth-utils.c create mode 100644 src/interfaces/libpq-oauth/oauth-utils.h create mode 100644 src/interfaces/libpq-oauth/po/LINGUAS create mode 100644 src/interfaces/libpq-oauth/po/meson.build diff --git a/meson.build b/meson.build index 454ed81f5ea..5620d959056 100644 --- a/meson.build +++ b/meson.build @@ -107,6 +107,7 @@ os_deps = [] backend_both_deps = [] backend_deps = [] libpq_deps = [] +libpq_oauth_deps = [] pg_sysroot = '' @@ -3215,17 +3216,18 @@ libpq_deps += [ gssapi, ldap_r, - # XXX libcurl must link after libgssapi_krb5 on FreeBSD to avoid segfaults - # during gss_acquire_cred(). This is possibly related to Curl's Heimdal - # dependency on that platform? - libcurl, libintl, ssl, ] +libpq_oauth_deps += [ + libcurl, +] + subdir('src/interfaces/libpq') -# fe_utils depends on libpq +# fe_utils and libpq-oauth depends on libpq subdir('src/fe_utils') +subdir('src/interfaces/libpq-oauth') # for frontend binaries frontend_code = declare_dependency( diff --git a/src/interfaces/Makefile b/src/interfaces/Makefile index 7d56b29d28f..322a498823d 100644 --- a/src/interfaces/Makefile +++ b/src/interfaces/Makefile @@ -14,7 +14,16 @@ include $(top_builddir)/src/Makefile.global SUBDIRS = libpq ecpg +ifeq ($(with_libcurl), yes) +SUBDIRS += libpq-oauth +endif + $(recurse) all-ecpg-recurse: all-libpq-recurse install-ecpg-recurse: install-libpq-recurse + +ifeq ($(with_libcurl), yes) +all-libpq-oauth-recurse: all-libpq-recurse +install-libpq-oauth-recurse: install-libpq-recurse +endif diff --git a/src/interfaces/libpq-oauth/Makefile b/src/interfaces/libpq-oauth/Makefile new file mode 100644 index 00000000000..461c44b59c1 --- /dev/null +++ b/src/interfaces/libpq-oauth/Makefile @@ -0,0 +1,56 @@ +#------------------------------------------------------------------------- +# +# Makefile for libpq-oauth +# +# Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group +# Portions Copyright (c) 1994, Regents of the University of California +# +# src/interfaces/libpq-oauth/Makefile +# +#------------------------------------------------------------------------- + +subdir = src/interfaces/libpq-oauth +top_builddir = ../../.. +include $(top_builddir)/src/Makefile.global + +PGFILEDESC = "libpq-oauth - device authorization oauth support" + +# This is an internal module; we don't want an SONAME and therefore do not set +# SO_MAJOR_VERSION. (We still put the major version into the name, to make it +# obvious where the library belongs.) +NAME = libpq-oauth-$(MAJORVERSION) + +override CPPFLAGS := -I$(libpq_srcdir) -I$(top_builddir)/src/port $(CPPFLAGS) + +OBJS = \ + $(WIN32RES) \ + oauth-curl.o \ + oauth-utils.o + +SHLIB_LINK_INTERNAL = $(libpq_pgport_shlib) +SHLIB_LINK = -lcurl +SHLIB_PREREQS = submake-libpq + +SHLIB_EXPORTS = exports.txt + +PKG_CONFIG_REQUIRES_PRIVATE = libpq +# +# Make dependencies on pg_config_paths.h visible in all builds. +oauth-curl.o: oauth-curl.c $(top_builddir)/src/port/pg_config_paths.h + +$(top_builddir)/src/port/pg_config_paths.h: + $(MAKE) -C $(top_builddir)/src/port pg_config_paths.h + +all: all-lib + +# Shared library stuff +include $(top_srcdir)/src/Makefile.shlib + +install: all installdirs install-lib + +installdirs: installdirs-lib + +uninstall: uninstall-lib + +clean distclean: clean-lib + rm -f $(OBJS) diff --git a/src/interfaces/libpq-oauth/README b/src/interfaces/libpq-oauth/README new file mode 100644 index 00000000000..5006f405080 --- /dev/null +++ b/src/interfaces/libpq-oauth/README @@ -0,0 +1,18 @@ +libpq-oauth is an optional module implementing the Device Authorization flow for +OAuth clients (RFC 8628). It was originally developed as part of libpq core and +later split out as its own shared library in order to isolate its dependency on +libcurl. (End users who don't want the Curl dependency can simply choose not to +install this module.) + +If a connection string allows the use of OAuth, the server asks for it, and a +libpq client has not installed its own custom OAuth flow, libpq will attempt to +delay-load this module using dlopen() and the following ABI. Failure to load +results in a failed connection. + += Load-Time ABI = + +This module ABI is an internal implementation detail, so it's subject to change +without warning, even during minor releases (however unlikely). The compiled +version of libpq-oauth should always match the compiled version of libpq. + +TODO diff --git a/src/interfaces/libpq-oauth/exports.txt b/src/interfaces/libpq-oauth/exports.txt new file mode 100644 index 00000000000..3787b388e04 --- /dev/null +++ b/src/interfaces/libpq-oauth/exports.txt @@ -0,0 +1,4 @@ +# src/interfaces/libpq-oauth/exports.txt +pg_fe_run_oauth_flow 1 +pg_fe_cleanup_oauth_flow 2 +pg_g_threadlock 3 diff --git a/src/interfaces/libpq-oauth/meson.build b/src/interfaces/libpq-oauth/meson.build new file mode 100644 index 00000000000..1834afbf7a5 --- /dev/null +++ b/src/interfaces/libpq-oauth/meson.build @@ -0,0 +1,32 @@ +# Copyright (c) 2022-2025, PostgreSQL Global Development Group + +if not libcurl.found() or host_system == 'windows' + subdir_done() +endif + +libpq_oauth_sources = files( + 'oauth-curl.c', + 'oauth-utils.c', +) + +export_file = custom_target('libpq-oauth.exports', + kwargs: gen_export_kwargs, +) + +# port needs to be in include path due to pthread-win32.h +libpq_oauth_inc = include_directories('.', '../libpq', '../../port') + +# This is an internal module; we don't want an SONAME and therefore do not set +# SO_MAJOR_VERSION. (We still put the major version into the name, to make it +# obvious where the library belongs.) +libpq_oauth_so = shared_module('libpq-oauth-' + pg_version_major.to_string(), + libpq_oauth_sources, + include_directories: [libpq_oauth_inc, postgres_inc], + c_pch: pch_postgres_fe_h, + dependencies: [frontend_shlib_code, libpq, libpq_oauth_deps], + link_depends: export_file, + link_args: export_fmt.format(export_file.full_path()), + kwargs: default_lib_args, +) + +subdir('po', if_found: libintl) diff --git a/src/interfaces/libpq-oauth/nls.mk b/src/interfaces/libpq-oauth/nls.mk new file mode 100644 index 00000000000..eab3347ef60 --- /dev/null +++ b/src/interfaces/libpq-oauth/nls.mk @@ -0,0 +1,15 @@ +# src/interfaces/libpq-oauth/nls.mk +CATALOG_NAME = libpq-oauth +GETTEXT_FILES = oauth-curl.c \ + oauth-utils.c +GETTEXT_TRIGGERS = actx_error:2 \ + libpq_append_conn_error:2 \ + libpq_append_error:2 \ + libpq_gettext \ + libpq_ngettext:1,2 +GETTEXT_FLAGS = actx_error:2:c-format \ + libpq_append_conn_error:2:c-format \ + libpq_append_error:2:c-format \ + libpq_gettext:1:pass-c-format \ + libpq_ngettext:1:pass-c-format \ + libpq_ngettext:2:pass-c-format diff --git a/src/interfaces/libpq/fe-auth-oauth-curl.c b/src/interfaces/libpq-oauth/oauth-curl.c similarity index 99% rename from src/interfaces/libpq/fe-auth-oauth-curl.c rename to src/interfaces/libpq-oauth/oauth-curl.c index cd9c0323bb6..11d17ec1597 100644 --- a/src/interfaces/libpq/fe-auth-oauth-curl.c +++ b/src/interfaces/libpq-oauth/oauth-curl.c @@ -1,6 +1,6 @@ /*------------------------------------------------------------------------- * - * fe-auth-oauth-curl.c + * oauth-curl.c * The libcurl implementation of OAuth/OIDC authentication, using the * OAuth Device Authorization Grant (RFC 8628). * @@ -8,7 +8,7 @@ * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION - * src/interfaces/libpq/fe-auth-oauth-curl.c + * src/interfaces/libpq-oauth/oauth-curl.c * *------------------------------------------------------------------------- */ @@ -31,6 +31,8 @@ #include "fe-auth-oauth.h" #include "libpq-int.h" #include "mb/pg_wchar.h" +#include "oauth-curl.h" +#include "oauth-utils.h" /* * It's generally prudent to set a maximum response size to buffer in memory, @@ -2487,8 +2489,9 @@ prompt_user(struct async_ctx *actx, PGconn *conn) .verification_uri_complete = actx->authz.verification_uri_complete, .expires_in = actx->authz.expires_in, }; + PQauthDataHook_type hook = PQgetAuthDataHook(); - res = PQauthDataHook(PQAUTHDATA_PROMPT_OAUTH_DEVICE, conn, &prompt); + res = hook(PQAUTHDATA_PROMPT_OAUTH_DEVICE, conn, &prompt); if (!res) { diff --git a/src/interfaces/libpq-oauth/oauth-curl.h b/src/interfaces/libpq-oauth/oauth-curl.h new file mode 100644 index 00000000000..bcc1e737dcd --- /dev/null +++ b/src/interfaces/libpq-oauth/oauth-curl.h @@ -0,0 +1,23 @@ +/*------------------------------------------------------------------------- + * + * oauth-curl.h + * + * Definitions for OAuth Device Authorization module + * + * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/interfaces/libpq-oauth/oauth-curl.h + * + *------------------------------------------------------------------------- + */ + +#ifndef OAUTH_CURL_H +#define OAUTH_CURL_H + +#include "libpq-fe.h" + +extern PGDLLEXPORT PostgresPollingStatusType pg_fe_run_oauth_flow(PGconn *conn); +extern PGDLLEXPORT void pg_fe_cleanup_oauth_flow(PGconn *conn); + +#endif /* OAUTH_CURL_H */ diff --git a/src/interfaces/libpq-oauth/oauth-utils.c b/src/interfaces/libpq-oauth/oauth-utils.c new file mode 100644 index 00000000000..81f9c6dc247 --- /dev/null +++ b/src/interfaces/libpq-oauth/oauth-utils.c @@ -0,0 +1,190 @@ +/*------------------------------------------------------------------------- + * + * oauth-utils.c + * + * "Glue" helpers providing a copy of some internal APIs from libpq. At + * some point in the future, we might be able to deduplicate. + * + * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * IDENTIFICATION + * src/interfaces/libpq-oauth/oauth-utils.c + * + *------------------------------------------------------------------------- + */ + +#include "postgres_fe.h" + +#include "libpq-int.h" +#include "oauth-utils.h" +#include "pg_config_paths.h" + +pgthreadlock_t pg_g_threadlock; + +/* + * Append a formatted string to the error message buffer of the given + * connection, after translating it. A newline is automatically appended; the + * format should not end with a newline. + */ +void +libpq_append_conn_error(PGconn *conn, const char *fmt,...) +{ + int save_errno = errno; + bool done; + va_list args; + + Assert(fmt[strlen(fmt) - 1] != '\n'); + + if (PQExpBufferBroken(&conn->errorMessage)) + return; /* already failed */ + + /* Loop in case we have to retry after enlarging the buffer. */ + do + { + errno = save_errno; + va_start(args, fmt); + done = appendPQExpBufferVA(&conn->errorMessage, libpq_gettext(fmt), args); + va_end(args); + } while (!done); + + appendPQExpBufferChar(&conn->errorMessage, '\n'); +} + +/* + * Returns true if the PGOAUTHDEBUG=UNSAFE flag is set in the environment. + */ +bool +oauth_unsafe_debugging_enabled(void) +{ + const char *env = getenv("PGOAUTHDEBUG"); + + return (env && strcmp(env, "UNSAFE") == 0); +} + +int +pq_block_sigpipe(sigset_t *osigset, bool *sigpipe_pending) +{ + sigset_t sigpipe_sigset; + sigset_t sigset; + + sigemptyset(&sigpipe_sigset); + sigaddset(&sigpipe_sigset, SIGPIPE); + + /* Block SIGPIPE and save previous mask for later reset */ + SOCK_ERRNO_SET(pthread_sigmask(SIG_BLOCK, &sigpipe_sigset, osigset)); + if (SOCK_ERRNO) + return -1; + + /* We can have a pending SIGPIPE only if it was blocked before */ + if (sigismember(osigset, SIGPIPE)) + { + /* Is there a pending SIGPIPE? */ + if (sigpending(&sigset) != 0) + return -1; + + if (sigismember(&sigset, SIGPIPE)) + *sigpipe_pending = true; + else + *sigpipe_pending = false; + } + else + *sigpipe_pending = false; + + return 0; +} + +void +pq_reset_sigpipe(sigset_t *osigset, bool sigpipe_pending, bool got_epipe) +{ + int save_errno = SOCK_ERRNO; + int signo; + sigset_t sigset; + + /* Clear SIGPIPE only if none was pending */ + if (got_epipe && !sigpipe_pending) + { + if (sigpending(&sigset) == 0 && + sigismember(&sigset, SIGPIPE)) + { + sigset_t sigpipe_sigset; + + sigemptyset(&sigpipe_sigset); + sigaddset(&sigpipe_sigset, SIGPIPE); + + sigwait(&sigpipe_sigset, &signo); + } + } + + /* Restore saved block mask */ + pthread_sigmask(SIG_SETMASK, osigset, NULL); + + SOCK_ERRNO_SET(save_errno); +} + +#ifdef ENABLE_NLS + +static void +libpq_binddomain(void) +{ + /* + * At least on Windows, there are gettext implementations that fail if + * multiple threads call bindtextdomain() concurrently. Use a mutex and + * flag variable to ensure that we call it just once per process. It is + * not known that similar bugs exist on non-Windows platforms, but we + * might as well do it the same way everywhere. + */ + static volatile bool already_bound = false; + static pthread_mutex_t binddomain_mutex = PTHREAD_MUTEX_INITIALIZER; + + if (!already_bound) + { + /* bindtextdomain() does not preserve errno */ +#ifdef WIN32 + int save_errno = GetLastError(); +#else + int save_errno = errno; +#endif + + (void) pthread_mutex_lock(&binddomain_mutex); + + if (!already_bound) + { + const char *ldir; + + /* + * No relocatable lookup here because the calling executable could + * be anywhere + */ + ldir = getenv("PGLOCALEDIR"); + if (!ldir) + ldir = LOCALEDIR; + bindtextdomain(PG_TEXTDOMAIN("libpq-oauth"), ldir); + already_bound = true; + } + + (void) pthread_mutex_unlock(&binddomain_mutex); + +#ifdef WIN32 + SetLastError(save_errno); +#else + errno = save_errno; +#endif + } +} + +char * +libpq_gettext(const char *msgid) +{ + libpq_binddomain(); + return dgettext(PG_TEXTDOMAIN("libpq-oauth"), msgid); +} + +char * +libpq_ngettext(const char *msgid, const char *msgid_plural, unsigned long n) +{ + libpq_binddomain(); + return dngettext(PG_TEXTDOMAIN("libpq-oauth"), msgid, msgid_plural, n); +} + +#endif /* ENABLE_NLS */ diff --git a/src/interfaces/libpq-oauth/oauth-utils.h b/src/interfaces/libpq-oauth/oauth-utils.h new file mode 100644 index 00000000000..e5bd6b28b11 --- /dev/null +++ b/src/interfaces/libpq-oauth/oauth-utils.h @@ -0,0 +1,22 @@ +/*------------------------------------------------------------------------- + * + * oauth-utils.h + * + * Definitions providing missing libpq internal APIs + * + * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/interfaces/libpq-oauth/oauth-utils.h + * + *------------------------------------------------------------------------- + */ + +#include "libpq-int.h" + +extern PGDLLEXPORT pgthreadlock_t pg_g_threadlock; + +void libpq_append_conn_error(PGconn *conn, const char *fmt,...) pg_attribute_printf(2, 3); +bool oauth_unsafe_debugging_enabled(void); +int pq_block_sigpipe(sigset_t *osigset, bool *sigpipe_pending); +void pq_reset_sigpipe(sigset_t *osigset, bool sigpipe_pending, bool got_epipe); diff --git a/src/interfaces/libpq-oauth/po/LINGUAS b/src/interfaces/libpq-oauth/po/LINGUAS new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/interfaces/libpq-oauth/po/meson.build b/src/interfaces/libpq-oauth/po/meson.build new file mode 100644 index 00000000000..61b3807ac68 --- /dev/null +++ b/src/interfaces/libpq-oauth/po/meson.build @@ -0,0 +1,3 @@ +# Copyright (c) 2022-2025, PostgreSQL Global Development Group + +nls_targets += [i18n.gettext('libpq-oauth-' + pg_version_major.to_string())] diff --git a/src/interfaces/libpq/Makefile b/src/interfaces/libpq/Makefile index 90b0b65db6f..8cf8d9e54d8 100644 --- a/src/interfaces/libpq/Makefile +++ b/src/interfaces/libpq/Makefile @@ -64,10 +64,6 @@ OBJS += \ fe-secure-gssapi.o endif -ifeq ($(with_libcurl),yes) -OBJS += fe-auth-oauth-curl.o -endif - ifeq ($(PORTNAME), cygwin) override shlib = cyg$(NAME)$(DLSUFFIX) endif diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index d5143766858..0625cf39e9a 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -210,3 +210,4 @@ PQsetAuthDataHook 207 PQgetAuthDataHook 208 PQdefaultAuthDataHook 209 PQfullProtocolVersion 210 +appendPQExpBufferVA 211 diff --git a/src/interfaces/libpq/fe-auth-oauth.c b/src/interfaces/libpq/fe-auth-oauth.c index cf1a25e2ccc..ce15a5e8de1 100644 --- a/src/interfaces/libpq/fe-auth-oauth.c +++ b/src/interfaces/libpq/fe-auth-oauth.c @@ -15,6 +15,10 @@ #include "postgres_fe.h" +#ifndef WIN32 +#include +#endif + #include "common/base64.h" #include "common/hmac.h" #include "common/jsonapi.h" @@ -721,6 +725,44 @@ cleanup_user_oauth_flow(PGconn *conn) state->async_ctx = NULL; } +static bool +use_builtin_flow(PGconn *conn, fe_oauth_state *state) +{ +#ifdef WIN32 + return false; +#else + PostgresPollingStatusType (*flow) (PGconn *conn); + void (*cleanup) (PGconn *conn); + pgthreadlock_t *threadlock_copy; + + /* libpq-oauth is versioned in lockstep; we don't export a stable ABI. */ + state->builtin_flow = dlopen("libpq-oauth-" PG_MAJORVERSION DLSUFFIX, + RTLD_NOW | RTLD_LOCAL); + if (!state->builtin_flow) + { + fprintf(stderr, "failed dlopen: %s\n", dlerror()); // XXX + return false; + } + + flow = dlsym(state->builtin_flow, "pg_fe_run_oauth_flow"); + cleanup = dlsym(state->builtin_flow, "pg_fe_cleanup_oauth_flow"); + threadlock_copy = dlsym(state->builtin_flow, "pg_g_threadlock"); + + if (!(flow && cleanup && threadlock_copy)) + { + fprintf(stderr, "failed dlsym: %s\n", dlerror()); // XXX + dlclose(state->builtin_flow); + return false; + } + + conn->async_auth = flow; + conn->cleanup_async_auth = cleanup; + *threadlock_copy = pg_g_threadlock; + + return true; +#endif /* !WIN32 */ +} + /* * Chooses an OAuth client flow for the connection, which will retrieve a Bearer * token for presentation to the server. @@ -792,18 +834,10 @@ setup_token_request(PGconn *conn, fe_oauth_state *state) libpq_append_conn_error(conn, "user-defined OAuth flow failed"); goto fail; } - else + else if (!use_builtin_flow(conn, state)) { -#if USE_LIBCURL - /* Hand off to our built-in OAuth flow. */ - conn->async_auth = pg_fe_run_oauth_flow; - conn->cleanup_async_auth = pg_fe_cleanup_oauth_flow; - -#else libpq_append_conn_error(conn, "no custom OAuth flows are available, and libpq was not built with libcurl support"); goto fail; - -#endif } return true; diff --git a/src/interfaces/libpq/fe-auth-oauth.h b/src/interfaces/libpq/fe-auth-oauth.h index 3f1a7503a01..699ba42acc2 100644 --- a/src/interfaces/libpq/fe-auth-oauth.h +++ b/src/interfaces/libpq/fe-auth-oauth.h @@ -33,10 +33,10 @@ typedef struct PGconn *conn; void *async_ctx; + + void *builtin_flow; } fe_oauth_state; -extern PostgresPollingStatusType pg_fe_run_oauth_flow(PGconn *conn); -extern void pg_fe_cleanup_oauth_flow(PGconn *conn); extern void pqClearOAuthToken(PGconn *conn); extern bool oauth_unsafe_debugging_enabled(void); diff --git a/src/interfaces/libpq/meson.build b/src/interfaces/libpq/meson.build index 292fecf3320..47d38e9378f 100644 --- a/src/interfaces/libpq/meson.build +++ b/src/interfaces/libpq/meson.build @@ -38,10 +38,6 @@ if gssapi.found() ) endif -if libcurl.found() - libpq_sources += files('fe-auth-oauth-curl.c') -endif - export_file = custom_target('libpq.exports', kwargs: gen_export_kwargs, ) -- 2.34.1