Skip to content

Commit 1749a12

Browse files
committed
oauth: Remove expired timers from the multiplexer
In a case similar to the previous commit, an expired timer can remain permanently readable if Curl does not remove the timeout itself. Since that removal isn't guaranteed to happen in real-world situations, implement drain_timer_events() to reset the timer before calling into drive_request(). Moving to drain_timer_events() happens to fix a logic bug in the previous caller of timer_expired(), which treated an error condition as if the timer were expired instead of bailing out. The previous implementation of timer_expired() gave differing results for epoll and kqueue if the timer was reset. (For epoll, a reset timer was considered to be expired, and for kqueue it was not.) This didn't previously cause problems, since timer_expired() was only called while the timer was known to be set, but both implementations now use the kqueue logic. Reviewed-by: Thomas Munro <thomas.munro@gmail.com> Backpatch-through: 18 Discussion: https://postgr.es/m/CAOYmi+nDZxJHaWj9_jRSyf8uMToCADAmOfJEggsKW-kY7aUwHA@mail.gmail.com
1 parent 3d9c034 commit 1749a12

File tree

1 file changed

+68
-40
lines changed

1 file changed

+68
-40
lines changed

src/interfaces/libpq-oauth/oauth-curl.c

Lines changed: 68 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1536,40 +1536,20 @@ set_timer(struct async_ctx *actx, long timeout)
15361536

15371537
/*
15381538
* Returns 1 if the timeout in the multiplexer set has expired since the last
1539-
* call to set_timer(), 0 if the timer is still running, or -1 (with an
1540-
* actx_error() report) if the timer cannot be queried.
1539+
* call to set_timer(), 0 if the timer is either still running or disarmed, or
1540+
* -1 (with an actx_error() report) if the timer cannot be queried.
15411541
*/
15421542
static int
15431543
timer_expired(struct async_ctx *actx)
15441544
{
1545-
#if defined(HAVE_SYS_EPOLL_H)
1546-
struct itimerspec spec = {0};
1547-
1548-
if (timerfd_gettime(actx->timerfd, &spec) < 0)
1549-
{
1550-
actx_error(actx, "getting timerfd value: %m");
1551-
return -1;
1552-
}
1553-
1554-
/*
1555-
* This implementation assumes we're using single-shot timers. If you
1556-
* change to using intervals, you'll need to reimplement this function
1557-
* too, possibly with the read() or select() interfaces for timerfd.
1558-
*/
1559-
Assert(spec.it_interval.tv_sec == 0
1560-
&& spec.it_interval.tv_nsec == 0);
1561-
1562-
/* If the remaining time to expiration is zero, we're done. */
1563-
return (spec.it_value.tv_sec == 0
1564-
&& spec.it_value.tv_nsec == 0);
1565-
#elif defined(HAVE_SYS_EVENT_H)
1545+
#if defined(HAVE_SYS_EPOLL_H) || defined(HAVE_SYS_EVENT_H)
15661546
int res;
15671547

1568-
/* Is the timer queue ready? */
1548+
/* Is the timer ready? */
15691549
res = PQsocketPoll(actx->timerfd, 1 /* forRead */ , 0, 0);
15701550
if (res < 0)
15711551
{
1572-
actx_error(actx, "checking kqueue for timeout: %m");
1552+
actx_error(actx, "checking timer expiration: %m");
15731553
return -1;
15741554
}
15751555

@@ -1601,6 +1581,36 @@ register_timer(CURLM *curlm, long timeout, void *ctx)
16011581
return 0;
16021582
}
16031583

1584+
/*
1585+
* Removes any expired-timer event from the multiplexer. If was_expired is not
1586+
* NULL, it will contain whether or not the timer was expired at time of call.
1587+
*/
1588+
static bool
1589+
drain_timer_events(struct async_ctx *actx, bool *was_expired)
1590+
{
1591+
int res;
1592+
1593+
res = timer_expired(actx);
1594+
if (res < 0)
1595+
return false;
1596+
1597+
if (res > 0)
1598+
{
1599+
/*
1600+
* Timer is expired. We could drain the event manually from the
1601+
* timerfd, but it's easier to simply disable it; that keeps the
1602+
* platform-specific code in set_timer().
1603+
*/
1604+
if (!set_timer(actx, -1))
1605+
return false;
1606+
}
1607+
1608+
if (was_expired)
1609+
*was_expired = (res > 0);
1610+
1611+
return true;
1612+
}
1613+
16041614
/*
16051615
* Prints Curl request debugging information to stderr.
16061616
*
@@ -2804,6 +2814,22 @@ pg_fe_run_oauth_flow_impl(PGconn *conn)
28042814
{
28052815
PostgresPollingStatusType status;
28062816

2817+
/*
2818+
* Clear any expired timeout before calling back into
2819+
* Curl. Curl is not guaranteed to do this for us, because
2820+
* its API expects us to use single-shot (i.e.
2821+
* edge-triggered) timeouts, and ours are level-triggered
2822+
* via the mux.
2823+
*
2824+
* This can't be combined with the comb_multiplexer() call
2825+
* below: we might accidentally clear a short timeout that
2826+
* was both set and expired during the call to
2827+
* drive_request().
2828+
*/
2829+
if (!drain_timer_events(actx, NULL))
2830+
goto error_return;
2831+
2832+
/* Move the request forward. */
28072833
status = drive_request(actx);
28082834

28092835
if (status == PGRES_POLLING_FAILED)
@@ -2826,24 +2852,26 @@ pg_fe_run_oauth_flow_impl(PGconn *conn)
28262852
}
28272853

28282854
case OAUTH_STEP_WAIT_INTERVAL:
2829-
2830-
/*
2831-
* The client application is supposed to wait until our timer
2832-
* expires before calling PQconnectPoll() again, but that
2833-
* might not happen. To avoid sending a token request early,
2834-
* check the timer before continuing.
2835-
*/
2836-
if (!timer_expired(actx))
28372855
{
2838-
set_conn_altsock(conn, actx->timerfd);
2839-
return PGRES_POLLING_READING;
2840-
}
2856+
bool expired;
28412857

2842-
/* Disable the expired timer. */
2843-
if (!set_timer(actx, -1))
2844-
goto error_return;
2858+
/*
2859+
* The client application is supposed to wait until our
2860+
* timer expires before calling PQconnectPoll() again, but
2861+
* that might not happen. To avoid sending a token request
2862+
* early, check the timer before continuing.
2863+
*/
2864+
if (!drain_timer_events(actx, &expired))
2865+
goto error_return;
28452866

2846-
break;
2867+
if (!expired)
2868+
{
2869+
set_conn_altsock(conn, actx->timerfd);
2870+
return PGRES_POLLING_READING;
2871+
}
2872+
2873+
break;
2874+
}
28472875
}
28482876

28492877
/*

0 commit comments

Comments
 (0)