From 47fc34de68fe61b796f532f755a17331dff111e3 Mon Sep 17 00:00:00 2001 From: Jacob Champion Date: Wed, 26 Mar 2025 10:55:28 -0700 Subject: [PATCH] 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 | 53 +++++ src/interfaces/libpq-oauth/exports.txt | 3 + .../fe-auth-oauth-curl.c | 187 +++++++++++++++++- .../libpq-oauth/fe-auth-oauth-curl.h | 23 +++ src/interfaces/libpq-oauth/meson.build | 64 ++++++ src/interfaces/libpq-oauth/po/meson.build | 3 + src/interfaces/libpq/Makefile | 4 - src/interfaces/libpq/exports.txt | 3 + src/interfaces/libpq/fe-auth-oauth.c | 52 ++++- src/interfaces/libpq/fe-auth-oauth.h | 4 +- src/interfaces/libpq/fe-auth.h | 3 - src/interfaces/libpq/libpq-fe.h | 1 + src/interfaces/libpq/meson.build | 4 - 15 files changed, 389 insertions(+), 36 deletions(-) create mode 100644 src/interfaces/libpq-oauth/Makefile create mode 100644 src/interfaces/libpq-oauth/exports.txt rename src/interfaces/{libpq => libpq-oauth}/fe-auth-oauth-curl.c (94%) create mode 100644 src/interfaces/libpq-oauth/fe-auth-oauth-curl.h create mode 100644 src/interfaces/libpq-oauth/meson.build create mode 100644 src/interfaces/libpq-oauth/po/meson.build diff --git a/meson.build b/meson.build index 7cf518a2765..69e91529259 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 = '' @@ -3136,17 +3137,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..d623a4157e6 --- /dev/null +++ b/src/interfaces/libpq-oauth/Makefile @@ -0,0 +1,53 @@ +#------------------------------------------------------------------------- +# +# 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" +NAME = pq-oauth +SO_MAJOR_VERSION = 1 +SO_MINOR_VERSION = $(MAJORVERSION) + +override CPPFLAGS := -I$(libpq_srcdir) -I$(top_builddir)/src/port $(CPPFLAGS) + +OBJS = \ + $(WIN32RES) \ + fe-auth-oauth-curl.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. +fe-auth-oauth-curl.o: fe-auth-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/exports.txt b/src/interfaces/libpq-oauth/exports.txt new file mode 100644 index 00000000000..ac9333763c4 --- /dev/null +++ b/src/interfaces/libpq-oauth/exports.txt @@ -0,0 +1,3 @@ +# src/interfaces/libpq-oauth/exports.txt +pg_fe_run_oauth_flow 1 +pg_fe_cleanup_oauth_flow 2 diff --git a/src/interfaces/libpq/fe-auth-oauth-curl.c b/src/interfaces/libpq-oauth/fe-auth-oauth-curl.c similarity index 94% rename from src/interfaces/libpq/fe-auth-oauth-curl.c rename to src/interfaces/libpq-oauth/fe-auth-oauth-curl.c index 9e0e8a9f2be..556e436ee93 100644 --- a/src/interfaces/libpq/fe-auth-oauth-curl.c +++ b/src/interfaces/libpq-oauth/fe-auth-oauth-curl.c @@ -29,8 +29,10 @@ #include "common/jsonapi.h" #include "fe-auth.h" #include "fe-auth-oauth.h" +#include "fe-auth-oauth-curl.h" #include "libpq-int.h" #include "mb/pg_wchar.h" +#include "pg_config_paths.h" /* * It's generally prudent to set a maximum response size to buffer in memory, @@ -230,6 +232,173 @@ struct async_ctx bool debugging; /* can we give unsafe developer assistance? */ }; +#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"), 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"), msgid); +} + +char * +libpq_ngettext(const char *msgid, const char *msgid_plural, unsigned long n) +{ + libpq_binddomain(); + return dngettext(PG_TEXTDOMAIN("libpq"), msgid, msgid_plural, n); +} + +#endif /* ENABLE_NLS */ + +static void __libpq_append_conn_error(PGconn *conn, const char *fmt,...) pg_attribute_printf(2, 3); + +/* + * 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. + */ +static 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. + */ +static bool +__oauth_unsafe_debugging_enabled(void) +{ + const char *env = getenv("PGOAUTHDEBUG"); + + return (env && strcmp(env, "UNSAFE") == 0); +} + +static 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; +} +static 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); +} /* * Tears down the Curl handles and frees the async_ctx. */ @@ -252,7 +421,7 @@ free_async_ctx(PGconn *conn, struct async_ctx *actx) CURLMcode err = curl_multi_remove_handle(actx->curlm, actx->curl); if (err) - libpq_append_conn_error(conn, + __libpq_append_conn_error(conn, "libcurl easy handle removal failed: %s", curl_multi_strerror(err)); } @@ -272,7 +441,7 @@ free_async_ctx(PGconn *conn, struct async_ctx *actx) CURLMcode err = curl_multi_cleanup(actx->curlm); if (err) - libpq_append_conn_error(conn, + __libpq_append_conn_error(conn, "libcurl multi handle cleanup failed: %s", curl_multi_strerror(err)); } @@ -2556,7 +2725,7 @@ initialize_curl(PGconn *conn) goto done; else if (init_successful == PG_BOOL_NO) { - libpq_append_conn_error(conn, + __libpq_append_conn_error(conn, "curl_global_init previously failed during OAuth setup"); goto done; } @@ -2575,7 +2744,7 @@ initialize_curl(PGconn *conn) */ if (curl_global_init(CURL_GLOBAL_ALL & ~CURL_GLOBAL_WIN32) != CURLE_OK) { - libpq_append_conn_error(conn, + __libpq_append_conn_error(conn, "curl_global_init failed during OAuth setup"); init_successful = PG_BOOL_NO; goto done; @@ -2597,7 +2766,7 @@ initialize_curl(PGconn *conn) * In a downgrade situation, the damage is already done. Curl global * state may be corrupted. Be noisy. */ - libpq_append_conn_error(conn, "libcurl is no longer thread-safe\n" + __libpq_append_conn_error(conn, "libcurl is no longer thread-safe\n" "\tCurl initialization was reported thread-safe when libpq\n" "\twas compiled, but the currently installed version of\n" "\tlibcurl reports that it is not. Recompile libpq against\n" @@ -2649,7 +2818,7 @@ pg_fe_run_oauth_flow_impl(PGconn *conn) actx = calloc(1, sizeof(*actx)); if (!actx) { - libpq_append_conn_error(conn, "out of memory"); + __libpq_append_conn_error(conn, "out of memory"); return PGRES_POLLING_FAILED; } @@ -2657,7 +2826,7 @@ pg_fe_run_oauth_flow_impl(PGconn *conn) actx->timerfd = -1; /* Should we enable unsafe features? */ - actx->debugging = oauth_unsafe_debugging_enabled(); + actx->debugging = __oauth_unsafe_debugging_enabled(); state->async_ctx = actx; @@ -2895,7 +3064,7 @@ pg_fe_run_oauth_flow(PGconn *conn) * difficult corner case to exercise in practice, and unfortunately it's * not really clear whether it's necessary in all cases. */ - masked = (pq_block_sigpipe(&osigset, &sigpipe_pending) == 0); + masked = (__pq_block_sigpipe(&osigset, &sigpipe_pending) == 0); #endif result = pg_fe_run_oauth_flow_impl(conn); @@ -2907,7 +3076,7 @@ pg_fe_run_oauth_flow(PGconn *conn) * Undo the SIGPIPE mask. Assume we may have gotten EPIPE (we have no * way of knowing at this level). */ - pq_reset_sigpipe(&osigset, sigpipe_pending, true /* EPIPE, maybe */ ); + __pq_reset_sigpipe(&osigset, sigpipe_pending, true /* EPIPE, maybe */ ); } #endif diff --git a/src/interfaces/libpq-oauth/fe-auth-oauth-curl.h b/src/interfaces/libpq-oauth/fe-auth-oauth-curl.h new file mode 100644 index 00000000000..907f360d9d1 --- /dev/null +++ b/src/interfaces/libpq-oauth/fe-auth-oauth-curl.h @@ -0,0 +1,23 @@ +/*------------------------------------------------------------------------- + * + * fe-auth-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/fe-auth-oauth-curl.h + * + *------------------------------------------------------------------------- + */ + +#ifndef FE_AUTH_OAUTH_CURL_H +#define FE_AUTH_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 /* FE_AUTH_OAUTH_CURL_H */ diff --git a/src/interfaces/libpq-oauth/meson.build b/src/interfaces/libpq-oauth/meson.build new file mode 100644 index 00000000000..bd348a0afc4 --- /dev/null +++ b/src/interfaces/libpq-oauth/meson.build @@ -0,0 +1,64 @@ +# Copyright (c) 2022-2025, PostgreSQL Global Development Group + +if not libcurl.found() or host_system == 'windows' + subdir_done() +endif + +libpq_sources = files( + 'fe-auth-oauth-curl.c', +) +libpq_so_sources = [] # for shared lib, in addition to the above + +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') +libpq_c_args = ['-DSO_MAJOR_VERSION=1'] + +# Not using both_libraries() here as +# 1) resource files should only be in the shared library +# 2) we want the .pc file to include a dependency to {pgport,common}_static for +# libpq_st, and {pgport,common}_shlib for libpq_sh +# +# We could try to avoid building the source files twice, but it probably adds +# more complexity than its worth (reusing object files requires also linking +# to the library on windows or breaks precompiled headers). +libpq_oauth_st = static_library('libpq-oauth', + libpq_sources, + include_directories: [libpq_oauth_inc], + c_args: libpq_c_args, + c_pch: pch_postgres_fe_h, + dependencies: [frontend_stlib_code, libpq_deps], + kwargs: default_lib_args, +) + +libpq_oauth_so = shared_library('libpq-oauth', + libpq_sources + libpq_so_sources, + include_directories: [libpq_oauth_inc, postgres_inc], + c_args: libpq_c_args, + c_pch: pch_postgres_fe_h, + version: '1.' + pg_version_major.to_string(), + soversion: host_system != 'windows' ? '1' : '', + darwin_versions: ['1', '1.' + pg_version_major.to_string()], + 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, +) + +libpq_oauth = declare_dependency( + link_with: [libpq_oauth_so], + include_directories: [include_directories('.')] +) + +pkgconfig.generate( + name: 'libpq-oauth', + description: 'PostgreSQL libpq library, device authorization oauth support', + url: pg_url, + libraries: libpq_oauth, + libraries_private: [frontend_stlib_code, libpq_oauth_deps], +) + +subdir('po', if_found: libintl) diff --git a/src/interfaces/libpq-oauth/po/meson.build b/src/interfaces/libpq-oauth/po/meson.build new file mode 100644 index 00000000000..1ca1faaf726 --- /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' + '1' + '-' + 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..bc0ed85482a 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -210,3 +210,6 @@ PQsetAuthDataHook 207 PQgetAuthDataHook 208 PQdefaultAuthDataHook 209 PQfullProtocolVersion 210 +appendPQExpBufferVA 211 +pg_g_threadlock 212 +PQauthDataHook 213 diff --git a/src/interfaces/libpq/fe-auth-oauth.c b/src/interfaces/libpq/fe-auth-oauth.c index cf1a25e2ccc..55f980f3d05 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); + + state->builtin_flow = dlopen( +#if defined(__darwin__) + "libpq-oauth.1.dylib", +#else + "libpq-oauth.so.1", +#endif + 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"); + + if (!(flow && cleanup)) + { + fprintf(stderr, "failed dlsym: %s\n", dlerror()); // XXX + return false; + } + + conn->async_auth = flow; + conn->cleanup_async_auth = cleanup; + + 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/fe-auth.h b/src/interfaces/libpq/fe-auth.h index de98e0d20c4..1d4991f8996 100644 --- a/src/interfaces/libpq/fe-auth.h +++ b/src/interfaces/libpq/fe-auth.h @@ -18,9 +18,6 @@ #include "libpq-int.h" -extern PQauthDataHook_type PQauthDataHook; - - /* Prototypes for functions in fe-auth.c */ extern int pg_fe_sendauth(AuthRequest areq, int payloadlen, PGconn *conn, bool *async); diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 7d3a9df6fd5..696a6587dd4 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -812,6 +812,7 @@ typedef int (*PQauthDataHook_type) (PGauthData type, PGconn *conn, void *data); extern void PQsetAuthDataHook(PQauthDataHook_type hook); extern PQauthDataHook_type PQgetAuthDataHook(void); extern int PQdefaultAuthDataHook(PGauthData type, PGconn *conn, void *data); +extern PQauthDataHook_type PQauthDataHook; /* === in encnames.c === */ diff --git a/src/interfaces/libpq/meson.build b/src/interfaces/libpq/meson.build index 19f4a52a97a..02a88408e34 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