Skip to content

Commit ad61c53

Browse files
jchampioCommitfest Bot
authored andcommitted
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.
1 parent 5ecf4d0 commit ad61c53

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
@@ -1588,40 +1588,20 @@ set_timer(struct async_ctx *actx, long timeout)
15881588

15891589
/*
15901590
* Returns 1 if the timeout in the multiplexer set has expired since the last
1591-
* call to set_timer(), 0 if the timer is still running, or -1 (with an
1592-
* actx_error() report) if the timer cannot be queried.
1591+
* call to set_timer(), 0 if the timer is either still running or disarmed, or
1592+
* -1 (with an actx_error() report) if the timer cannot be queried.
15931593
*/
15941594
static int
15951595
timer_expired(struct async_ctx *actx)
15961596
{
1597-
#if defined(HAVE_SYS_EPOLL_H)
1598-
struct itimerspec spec = {0};
1599-
1600-
if (timerfd_gettime(actx->timerfd, &spec) < 0)
1601-
{
1602-
actx_error(actx, "getting timerfd value: %m");
1603-
return -1;
1604-
}
1605-
1606-
/*
1607-
* This implementation assumes we're using single-shot timers. If you
1608-
* change to using intervals, you'll need to reimplement this function
1609-
* too, possibly with the read() or select() interfaces for timerfd.
1610-
*/
1611-
Assert(spec.it_interval.tv_sec == 0
1612-
&& spec.it_interval.tv_nsec == 0);
1613-
1614-
/* If the remaining time to expiration is zero, we're done. */
1615-
return (spec.it_value.tv_sec == 0
1616-
&& spec.it_value.tv_nsec == 0);
1617-
#elif defined(HAVE_SYS_EVENT_H)
1597+
#if defined(HAVE_SYS_EPOLL_H) || defined(HAVE_SYS_EVENT_H)
16181598
int res;
16191599

1620-
/* Is the timer queue ready? */
1600+
/* Is the timer ready? */
16211601
res = PQsocketPoll(actx->timerfd, 1 /* forRead */ , 0, 0);
16221602
if (res < 0)
16231603
{
1624-
actx_error(actx, "checking kqueue for timeout: %m");
1604+
actx_error(actx, "checking timer expiration: %m");
16251605
return -1;
16261606
}
16271607

@@ -1653,6 +1633,36 @@ register_timer(CURLM *curlm, long timeout, void *ctx)
16531633
return 0;
16541634
}
16551635

1636+
/*
1637+
* Removes any expired-timer event from the multiplexer. If was_expired is not
1638+
* NULL, it will contain whether or not the timer was expired at time of call.
1639+
*/
1640+
static bool
1641+
drain_timer_events(struct async_ctx *actx, bool *was_expired)
1642+
{
1643+
int res;
1644+
1645+
res = timer_expired(actx);
1646+
if (res < 0)
1647+
return false;
1648+
1649+
if (res > 0)
1650+
{
1651+
/*
1652+
* Timer is expired. We could drain the event manually from the
1653+
* timerfd, but it's easier to simply disable it; that keeps the
1654+
* platform-specific code in set_timer().
1655+
*/
1656+
if (!set_timer(actx, -1))
1657+
return false;
1658+
}
1659+
1660+
if (was_expired)
1661+
*was_expired = (res > 0);
1662+
1663+
return true;
1664+
}
1665+
16561666
/*
16571667
* Prints Curl request debugging information to stderr.
16581668
*
@@ -2856,6 +2866,22 @@ pg_fe_run_oauth_flow_impl(PGconn *conn)
28562866
{
28572867
PostgresPollingStatusType status;
28582868

2869+
/*
2870+
* Clear any expired timeout before calling back into
2871+
* Curl. Curl is not guaranteed to do this for us, because
2872+
* its API expects us to use single-shot (i.e.
2873+
* edge-triggered) timeouts, and ours are level-triggered
2874+
* via the mux.
2875+
*
2876+
* This can't be combined with the drain_socket_events()
2877+
* call below: we might accidentally clear a short timeout
2878+
* that was both set and expired during the call to
2879+
* drive_request().
2880+
*/
2881+
if (!drain_timer_events(actx, NULL))
2882+
goto error_return;
2883+
2884+
/* Move the request forward. */
28592885
status = drive_request(actx);
28602886

28612887
if (status == PGRES_POLLING_FAILED)
@@ -2879,24 +2905,26 @@ pg_fe_run_oauth_flow_impl(PGconn *conn)
28792905
}
28802906

28812907
case OAUTH_STEP_WAIT_INTERVAL:
2882-
2883-
/*
2884-
* The client application is supposed to wait until our timer
2885-
* expires before calling PQconnectPoll() again, but that
2886-
* might not happen. To avoid sending a token request early,
2887-
* check the timer before continuing.
2888-
*/
2889-
if (!timer_expired(actx))
28902908
{
2891-
set_conn_altsock(conn, actx->timerfd);
2892-
return PGRES_POLLING_READING;
2893-
}
2909+
bool expired;
28942910

2895-
/* Disable the expired timer. */
2896-
if (!set_timer(actx, -1))
2897-
goto error_return;
2911+
/*
2912+
* The client application is supposed to wait until our
2913+
* timer expires before calling PQconnectPoll() again, but
2914+
* that might not happen. To avoid sending a token request
2915+
* early, check the timer before continuing.
2916+
*/
2917+
if (!drain_timer_events(actx, &expired))
2918+
goto error_return;
28982919

2899-
break;
2920+
if (!expired)
2921+
{
2922+
set_conn_altsock(conn, actx->timerfd);
2923+
return PGRES_POLLING_READING;
2924+
}
2925+
2926+
break;
2927+
}
29002928
}
29012929

29022930
/*

0 commit comments

Comments
 (0)