diff options
author | Heikki Linnakangas | 2015-02-02 15:08:45 +0000 |
---|---|---|
committer | Heikki Linnakangas | 2015-02-02 15:09:53 +0000 |
commit | 2b3a8b20c2da9f39ffecae25ab7c66974fbc0d3b (patch) | |
tree | f0294b5a71c2b3c847446f4145d5d2a3247de8bb /src/backend/tcop/postgres.c | |
parent | 59b919822ab060f721e235964d19b55a19c815f0 (diff) |
Be more careful to not lose sync in the FE/BE protocol.
If any error occurred while we were in the middle of reading a protocol
message from the client, we could lose sync, and incorrectly try to
interpret a part of another message as a new protocol message. That will
usually lead to an "invalid frontend message" error that terminates the
connection. However, this is a security issue because an attacker might
be able to deliberately cause an error, inject a Query message in what's
supposed to be just user data, and have the server execute it.
We were quite careful to not have CHECK_FOR_INTERRUPTS() calls or other
operations that could ereport(ERROR) in the middle of processing a message,
but a query cancel interrupt or statement timeout could nevertheless cause
it to happen. Also, the V2 fastpath and COPY handling were not so careful.
It's very difficult to recover in the V2 COPY protocol, so we will just
terminate the connection on error. In practice, that's what happened
previously anyway, as we lost protocol sync.
To fix, add a new variable in pqcomm.c, PqCommReadingMsg, that is set
whenever we're in the middle of reading a message. When it's set, we cannot
safely ERROR out and continue running, because we might've read only part
of a message. PqCommReadingMsg acts somewhat similarly to critical sections
in that if an error occurs while it's set, the error handler will force the
connection to be terminated, as if the error was FATAL. It's not
implemented by promoting ERROR to FATAL in elog.c, like ERROR is promoted
to PANIC in critical sections, because we want to be able to use
PG_TRY/CATCH to recover and regain protocol sync. pq_getmessage() takes
advantage of that to prevent an OOM error from terminating the connection.
To prevent unnecessary connection terminations, add a holdoff mechanism
similar to HOLD/RESUME_INTERRUPTS() that can be used hold off query cancel
interrupts, but still allow die interrupts. The rules on which interrupts
are processed when are now a bit more complicated, so refactor
ProcessInterrupts() and the calls to it in signal handlers so that the
signal handlers always call it if ImmediateInterruptOK is set, and
ProcessInterrupts() can decide to not do anything if the other conditions
are not met.
Reported by Emil Lenngren. Patch reviewed by Noah Misch and Andres Freund.
Backpatch to all supported versions.
Security: CVE-2015-0244
Diffstat (limited to 'src/backend/tcop/postgres.c')
-rw-r--r-- | src/backend/tcop/postgres.c | 166 |
1 files changed, 112 insertions, 54 deletions
diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c index 8f743536cff..b82c3b333bd 100644 --- a/src/backend/tcop/postgres.c +++ b/src/backend/tcop/postgres.c @@ -331,6 +331,8 @@ SocketBackend(StringInfo inBuf) /* * Get message type code from the frontend. */ + HOLD_CANCEL_INTERRUPTS(); + pq_startmsgread(); qtype = pq_getbyte(); if (qtype == EOF) /* frontend disconnected */ @@ -379,7 +381,7 @@ SocketBackend(StringInfo inBuf) { /* * Can't send DEBUG log messages to client at this - * point.Since we're disconnecting right away, we + * point. Since we're disconnecting right away, we * don't need to restore whereToSendOutput. */ whereToSendOutput = DestNone; @@ -393,8 +395,30 @@ SocketBackend(StringInfo inBuf) break; case 'F': /* fastpath function call */ - /* we let fastpath.c cope with old-style input of this */ doing_extended_query_message = false; + if (PG_PROTOCOL_MAJOR(FrontendProtocol) < 3) + { + if (GetOldFunctionMessage(inBuf)) + { + if (IsTransactionState()) + ereport(COMMERROR, + (errcode(ERRCODE_CONNECTION_FAILURE), + errmsg("unexpected EOF on client connection with an open transaction"))); + else + { + /* + * Can't send DEBUG log messages to client at this + * point. Since we're disconnecting right away, we + * don't need to restore whereToSendOutput. + */ + whereToSendOutput = DestNone; + ereport(DEBUG1, + (errcode(ERRCODE_CONNECTION_DOES_NOT_EXIST), + errmsg("unexpected EOF on client connection"))); + } + return EOF; + } + } break; case 'X': /* terminate */ @@ -462,6 +486,9 @@ SocketBackend(StringInfo inBuf) if (pq_getmessage(inBuf, 0)) return EOF; /* suitable message already logged */ } + else + pq_endmsgread(); + RESUME_CANCEL_INTERRUPTS(); return qtype; } @@ -506,7 +533,7 @@ prepare_for_client_read(void) EnableNotifyInterrupt(); EnableCatchupInterrupt(); - /* Allow cancel/die interrupts to be processed while waiting */ + /* Allow die interrupts to be processed while waiting */ ImmediateInterruptOK = true; /* And don't forget to detect one that already arrived */ @@ -2589,21 +2616,11 @@ die(SIGNAL_ARGS) ProcDiePending = true; /* - * If it's safe to interrupt, and we're waiting for input or a lock, - * service the interrupt immediately + * If we're waiting for input or a lock so that it's safe to + * interrupt, service the interrupt immediately */ - if (ImmediateInterruptOK && InterruptHoldoffCount == 0 && - CritSectionCount == 0) - { - /* bump holdoff count to make ProcessInterrupts() a no-op */ - /* until we are done getting ready for it */ - InterruptHoldoffCount++; - LockErrorCleanup(); /* prevent CheckDeadLock from running */ - DisableNotifyInterrupt(); - DisableCatchupInterrupt(); - InterruptHoldoffCount--; + if (ImmediateInterruptOK) ProcessInterrupts(); - } } /* If we're still here, waken anything waiting on the process latch */ @@ -2630,21 +2647,11 @@ StatementCancelHandler(SIGNAL_ARGS) QueryCancelPending = true; /* - * If it's safe to interrupt, and we're waiting for input or a lock, - * service the interrupt immediately + * If we're waiting for input or a lock so that it's safe to + * interrupt, service the interrupt immediately */ - if (ImmediateInterruptOK && InterruptHoldoffCount == 0 && - CritSectionCount == 0) - { - /* bump holdoff count to make ProcessInterrupts() a no-op */ - /* until we are done getting ready for it */ - InterruptHoldoffCount++; - LockErrorCleanup(); /* prevent CheckDeadLock from running */ - DisableNotifyInterrupt(); - DisableCatchupInterrupt(); - InterruptHoldoffCount--; + if (ImmediateInterruptOK) ProcessInterrupts(); - } } /* If we're still here, waken anything waiting on the process latch */ @@ -2787,21 +2794,11 @@ RecoveryConflictInterrupt(ProcSignalReason reason) RecoveryConflictRetryable = false; /* - * If it's safe to interrupt, and we're waiting for input or a lock, - * service the interrupt immediately + * If we're waiting for input or a lock so that it's safe to + * interrupt, service the interrupt immediately. */ - if (ImmediateInterruptOK && InterruptHoldoffCount == 0 && - CritSectionCount == 0) - { - /* bump holdoff count to make ProcessInterrupts() a no-op */ - /* until we are done getting ready for it */ - InterruptHoldoffCount++; - LockErrorCleanup(); /* prevent CheckDeadLock from running */ - DisableNotifyInterrupt(); - DisableCatchupInterrupt(); - InterruptHoldoffCount--; + if (ImmediateInterruptOK) ProcessInterrupts(); - } } /* @@ -2826,15 +2823,17 @@ RecoveryConflictInterrupt(ProcSignalReason reason) void ProcessInterrupts(void) { - /* OK to accept interrupt now? */ + /* OK to accept any interrupts now? */ if (InterruptHoldoffCount != 0 || CritSectionCount != 0) return; InterruptPending = false; + if (ProcDiePending) { ProcDiePending = false; QueryCancelPending = false; /* ProcDie trumps QueryCancel */ ImmediateInterruptOK = false; /* not idle anymore */ + LockErrorCleanup(); DisableNotifyInterrupt(); DisableCatchupInterrupt(); /* As in quickdie, don't risk sending to client during auth */ @@ -2871,6 +2870,7 @@ ProcessInterrupts(void) { QueryCancelPending = false; /* lost connection trumps QueryCancel */ ImmediateInterruptOK = false; /* not idle anymore */ + LockErrorCleanup(); DisableNotifyInterrupt(); DisableCatchupInterrupt(); /* don't send to client, we already know the connection to be dead. */ @@ -2879,12 +2879,53 @@ ProcessInterrupts(void) (errcode(ERRCODE_CONNECTION_FAILURE), errmsg("connection to client lost"))); } + + /* + * If a recovery conflict happens while we are waiting for input from the + * client, the client is presumably just sitting idle in a transaction, + * preventing recovery from making progress. Terminate the connection to + * dislodge it. + */ + if (RecoveryConflictPending && DoingCommandRead) + { + QueryCancelPending = false; /* this trumps QueryCancel */ + ImmediateInterruptOK = false; /* not idle anymore */ + RecoveryConflictPending = false; + LockErrorCleanup(); + DisableNotifyInterrupt(); + DisableCatchupInterrupt(); + pgstat_report_recovery_conflict(RecoveryConflictReason); + ereport(FATAL, + (errcode(ERRCODE_T_R_SERIALIZATION_FAILURE), + errmsg("terminating connection due to conflict with recovery"), + errdetail_recovery_conflict(), + errhint("In a moment you should be able to reconnect to the" + " database and repeat your command."))); + } + if (QueryCancelPending) { + /* + * Don't allow query cancel interrupts while reading input from the + * client, because we might lose sync in the FE/BE protocol. (Die + * interrupts are OK, because we won't read any further messages from + * the client in that case.) + */ + if (QueryCancelHoldoffCount != 0) + { + /* + * Re-arm InterruptPending so that we process the cancel request + * as soon as we're done reading the message. + */ + InterruptPending = true; + return; + } + QueryCancelPending = false; if (ClientAuthInProgress) { ImmediateInterruptOK = false; /* not idle anymore */ + LockErrorCleanup(); DisableNotifyInterrupt(); DisableCatchupInterrupt(); /* As in quickdie, don't risk sending to client during auth */ @@ -2903,6 +2944,7 @@ ProcessInterrupts(void) { ImmediateInterruptOK = false; /* not idle anymore */ (void) get_timeout_indicator(STATEMENT_TIMEOUT, true); + LockErrorCleanup(); DisableNotifyInterrupt(); DisableCatchupInterrupt(); ereport(ERROR, @@ -2912,6 +2954,7 @@ ProcessInterrupts(void) if (get_timeout_indicator(STATEMENT_TIMEOUT, true)) { ImmediateInterruptOK = false; /* not idle anymore */ + LockErrorCleanup(); DisableNotifyInterrupt(); DisableCatchupInterrupt(); ereport(ERROR, @@ -2921,6 +2964,7 @@ ProcessInterrupts(void) if (IsAutoVacuumWorkerProcess()) { ImmediateInterruptOK = false; /* not idle anymore */ + LockErrorCleanup(); DisableNotifyInterrupt(); DisableCatchupInterrupt(); ereport(ERROR, @@ -2931,21 +2975,14 @@ ProcessInterrupts(void) { ImmediateInterruptOK = false; /* not idle anymore */ RecoveryConflictPending = false; + LockErrorCleanup(); DisableNotifyInterrupt(); DisableCatchupInterrupt(); pgstat_report_recovery_conflict(RecoveryConflictReason); - if (DoingCommandRead) - ereport(FATAL, - (errcode(ERRCODE_T_R_SERIALIZATION_FAILURE), - errmsg("terminating connection due to conflict with recovery"), - errdetail_recovery_conflict(), - errhint("In a moment you should be able to reconnect to the" - " database and repeat your command."))); - else - ereport(ERROR, - (errcode(ERRCODE_T_R_SERIALIZATION_FAILURE), + ereport(ERROR, + (errcode(ERRCODE_T_R_SERIALIZATION_FAILURE), errmsg("canceling statement due to conflict with recovery"), - errdetail_recovery_conflict())); + errdetail_recovery_conflict())); } /* @@ -2956,6 +2993,7 @@ ProcessInterrupts(void) if (!DoingCommandRead) { ImmediateInterruptOK = false; /* not idle anymore */ + LockErrorCleanup(); DisableNotifyInterrupt(); DisableCatchupInterrupt(); ereport(ERROR, @@ -3862,6 +3900,19 @@ PostgresMain(int argc, char *argv[], /* We don't have a transaction command open anymore */ xact_started = false; + /* + * If an error occurred while we were reading a message from the + * client, we have potentially lost track of where the previous + * message ends and the next one begins. Even though we have + * otherwise recovered from the error, we cannot safely read any more + * messages from the client, so there isn't much we can do with the + * connection anymore. + */ + if (pq_is_reading_msg()) + ereport(FATAL, + (errcode(ERRCODE_PROTOCOL_VIOLATION), + errmsg("terminating connection because protocol sync was lost"))); + /* Now we can allow interrupts again */ RESUME_INTERRUPTS(); } @@ -3946,7 +3997,14 @@ PostgresMain(int argc, char *argv[], /* * (4) disable async signal conditions again. + * + * Query cancel is supposed to be a no-op when there is no query in + * progress, so if a query cancel arrived while we were idle, just + * reset QueryCancelPending. ProcessInterrupts() has that effect when + * it's called when DoingCommandRead is set, so check for interrupts + * before resetting DoingCommandRead. */ + CHECK_FOR_INTERRUPTS(); DoingCommandRead = false; /* |