diff options
Diffstat (limited to 'src/libpcp/src/pdu.c')
-rw-r--r-- | src/libpcp/src/pdu.c | 574 |
1 files changed, 574 insertions, 0 deletions
diff --git a/src/libpcp/src/pdu.c b/src/libpcp/src/pdu.c new file mode 100644 index 0000000..3036976 --- /dev/null +++ b/src/libpcp/src/pdu.c @@ -0,0 +1,574 @@ +/* + * Copyright (c) 2012-2014 Red Hat. + * Copyright (c) 1995-2005 Silicon Graphics, Inc. All Rights Reserved. + * + * This library is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as published + * by the Free Software Foundation; either version 2.1 of the License, or + * (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public + * License for more details. + * + * Thread-safe notes: + * + * maxsize - monotonic increasing and rarely changes, so use global + * mutex to protect updates, but allow reads without locking + * as seeing an unexpected newly updated value is benign + * + * On success, the result parameter from __pmGetPDU() points into a PDU + * buffer that is pinned from the call to __pmFindPDUBuf(). It is the + * responsibility of the __pmGetPDU() caller to unpin the buffer when + * it is safe to do so. + * + * __pmPDUCntIn[] and __pmPDUCntOut[] are diagnostic counters that are + * maintained with non-atomic updates ... we've decided that it is + * acceptable for their values to be subject to possible (but unlikely) + * missed updates + */ + +#include "pmapi.h" +#include "impl.h" +#include "internal.h" + +INTERN int pmDebug; /* the real McCoy */ + +/* + * Performance Instrumentation + * ... counts binary PDUs received and sent by low 4 bits of PDU type + */ + +static unsigned int inctrs[PDU_MAX+1]; +static unsigned int outctrs[PDU_MAX+1]; +INTERN unsigned int *__pmPDUCntIn = inctrs; +INTERN unsigned int *__pmPDUCntOut = outctrs; + +#ifdef PCP_DEBUG +static int mypid = -1; +#endif +static int ceiling = PDU_CHUNK * 64; + +static struct timeval def_wait = { 10, 0 }; +static double def_timeout = 10.0; + +#define HEADER -1 +#define BODY 0 + +const struct timeval * +__pmDefaultRequestTimeout(void) +{ + static int done_default = 0; + + PM_INIT_LOCKS(); + PM_LOCK(__pmLock_libpcp); + if (!done_default) { + char *timeout_str; + char *end_ptr; + if ((timeout_str = getenv("PMCD_REQUEST_TIMEOUT")) != NULL) { + def_timeout = strtod(timeout_str, &end_ptr); + if (*end_ptr != '\0' || def_timeout < 0.0) { + __pmNotifyErr(LOG_WARNING, + "ignored bad PMCD_REQUEST_TIMEOUT = '%s'\n", + timeout_str); + } + else { + def_wait.tv_sec = (int)def_timeout; /* truncate -> secs */ + if (def_timeout > (double)def_wait.tv_sec) + def_wait.tv_usec = (long)((def_timeout - (double)def_wait.tv_sec) * 1000000); + else + def_wait.tv_usec = 0; + } + } + done_default = 1; + } + PM_UNLOCK(__pmLock_libpcp); + return (&def_wait); +} + +static int +pduread(int fd, char *buf, int len, int part, int timeout) +{ + int socketipc = __pmSocketIPC(fd); + int status = 0; + int have = 0; + int onetrip = 1; + struct timeval dead_hand; + struct timeval now; + + if (timeout == -2 /*TIMEOUT_ASYNC*/) + return -EOPNOTSUPP; + + /* + * Handle short reads that may split a PDU ... + * + * The original logic here assumed that recv() would only split a + * PDU at a word (__pmPDU) boundary ... with the introduction of + * secure connections, SSL and possibly compression all lurking + * below the socket covers, this is no longer a safe assumption. + * + * So, we keep nibbling at the input stream until we have all that + * we have requested, or we timeout, or error. + */ + while (len) { + struct timeval wait; + +#if defined(IS_MINGW) /* cannot select on a pipe on Win32 - yay! */ + if (!__pmSocketIPC(fd)) { + COMMTIMEOUTS cwait = { 0 }; + + if (timeout != TIMEOUT_NEVER) + cwait.ReadTotalTimeoutConstant = timeout * 1000.0; + else + cwait.ReadTotalTimeoutConstant = def_timeout * 1000.0; + SetCommTimeouts((HANDLE)_get_osfhandle(fd), &cwait); + } + else +#endif + + /* + * either never timeout (i.e. block forever), or timeout + */ + if (timeout != TIMEOUT_NEVER) { + if (timeout > 0) { + wait.tv_sec = timeout; + wait.tv_usec = 0; + } + else + wait = def_wait; + if (onetrip) { + /* + * Need all parts of the PDU to be received by dead_hand + * This enforces a low overall timeout for the whole PDU + * (as opposed to just a timeout for individual calls to + * recv). A more invasive alternative (better) approach + * would see all I/O performed in the main event loop, + * and I/O routines transformed to continuation-passing + * style. + */ + gettimeofday(&dead_hand, NULL); + dead_hand.tv_sec += wait.tv_sec; + dead_hand.tv_usec += wait.tv_usec; + while (dead_hand.tv_usec >= 1000000) { + dead_hand.tv_usec -= 1000000; + dead_hand.tv_sec++; + } + onetrip = 0; + } + + status = __pmSocketReady(fd, &wait); + if (status > 0) { + gettimeofday(&now, NULL); + if (now.tv_sec > dead_hand.tv_sec || + (now.tv_sec == dead_hand.tv_sec && + now.tv_usec > dead_hand.tv_usec)) + status = 0; + } + if (status == 0) { + if (__pmGetInternalState() != PM_STATE_APPL) { + /* special for PMCD and friends + * Note, on Linux select would return 'time remaining' + * in timeout value, so report the expected timeout + */ + int tosec, tomsec; + + if ( timeout != TIMEOUT_NEVER && timeout > 0 ) { + tosec = (int)timeout; + tomsec = 0; + } else { + tosec = (int)def_wait.tv_sec; + tomsec = 1000*(int)def_wait.tv_usec; + } + + __pmNotifyErr(LOG_WARNING, + "pduread: timeout (after %d.%03d " + "sec) while attempting to read %d " + "bytes out of %d in %s on fd=%d", + tosec, tomsec, len - have, len, + part == HEADER ? "HDR" : "BODY", fd); + } + return PM_ERR_TIMEOUT; + } + else if (status < 0) { + char errmsg[PM_MAXERRMSGLEN]; + __pmNotifyErr(LOG_ERR, "pduread: select() on fd=%d: %s", + fd, netstrerror_r(errmsg, sizeof(errmsg))); + setoserror(neterror()); + return status; + } + } + if (socketipc) { + status = __pmRecv(fd, buf, len, 0); + setoserror(neterror()); + } else { + status = read(fd, buf, len); + } + __pmOverrideLastFd(fd); + if (status < 0) + /* error */ + return status; + else if (status == 0) + /* return what we have, or nothing */ + break; + + have += status; + buf += status; + len -= status; +#ifdef PCP_DEBUG + if ((pmDebug & DBG_TRACE_PDU) && (pmDebug & DBG_TRACE_DESPERATE)) { + fprintf(stderr, "pduread(%d, ...): have %d, last read %d, still need %d\n", + fd, have, status, len); + } +#endif + } + + return have; +} + +char * +__pmPDUTypeStr_r(int type, char *buf, int buflen) +{ + char *res = NULL; + if (type == PDU_ERROR) res = "ERROR"; + else if (type == PDU_RESULT) res = "RESULT"; + else if (type == PDU_PROFILE) res = "PROFILE"; + else if (type == PDU_FETCH) res = "FETCH"; + else if (type == PDU_DESC_REQ) res = "DESC_REQ"; + else if (type == PDU_DESC) res = "DESC"; + else if (type == PDU_INSTANCE_REQ) res = "INSTANCE_REQ"; + else if (type == PDU_INSTANCE) res = "INSTANCE"; + else if (type == PDU_TEXT_REQ) res = "TEXT_REQ"; + else if (type == PDU_TEXT) res = "TEXT"; + else if (type == PDU_CONTROL_REQ) res = "CONTROL_REQ"; + else if (type == PDU_CREDS) res = "CREDS"; + else if (type == PDU_PMNS_IDS) res = "PMNS_IDS"; + else if (type == PDU_PMNS_NAMES) res = "PMNS_NAMES"; + else if (type == PDU_PMNS_CHILD) res = "PMNS_CHILD"; + else if (type == PDU_PMNS_TRAVERSE) res = "PMNS_TRAVERSE"; + else if (type == PDU_LOG_CONTROL) res = "LOG_CONTROL"; + else if (type == PDU_LOG_STATUS) res = "LOG_STATUS"; + else if (type == PDU_LOG_REQUEST) res = "LOG_REQUEST"; + else if (type == PDU_AUTH) res = "AUTH"; + if (res == NULL) + snprintf(buf, buflen, "TYPE-%d?", type); + else + snprintf(buf, buflen, "%s", res); + + return buf; +} + +const char * +__pmPDUTypeStr(int type) +{ + static char tbuf[20]; + __pmPDUTypeStr_r(type, tbuf, sizeof(tbuf)); + return tbuf; +} + +#if defined(HAVE_SIGPIPE) +/* + * Because the default handler for SIGPIPE is to exit, we always want a handler + * installed to override that so that the write() just returns an error. The + * problem is that the user might have installed one prior to the first write() + * or may install one at some later stage. This doesn't matter. As long as a + * handler other than SIG_DFL is there, all will be well. The first time that + * __pmXmitPDU is called, install SIG_IGN as the handler for SIGPIPE. If the + * user had already changed the handler from SIG_DFL, put back what was there + * before. + */ +static int sigpipe_done = 0; /* First time check for installation of + non-default SIGPIPE handler */ +static void setup_sigpipe() +{ + if (!sigpipe_done) { /* Make sure SIGPIPE is handled */ + SIG_PF user_onpipe; + user_onpipe = signal(SIGPIPE, SIG_IGN); + if (user_onpipe != SIG_DFL) /* Put user handler back */ + signal(SIGPIPE, user_onpipe); + sigpipe_done = 1; + } +} +#else +static void setup_sigpipe() { } +#endif + +int +__pmXmitPDU(int fd, __pmPDU *pdubuf) +{ + int socketipc = __pmSocketIPC(fd); + int off = 0; + int len; + __pmPDUHdr *php = (__pmPDUHdr *)pdubuf; + + setup_sigpipe(); + +#ifdef PCP_DEBUG + if (pmDebug & DBG_TRACE_PDU) { + int j; + char *p; + int jend = PM_PDU_SIZE(php->len); + char strbuf[20]; + + /* for Purify ... */ + p = (char *)pdubuf + php->len; + while (p < (char *)pdubuf + jend*sizeof(__pmPDU)) + *p++ = '~'; /* buffer end */ + + if (mypid == -1) + mypid = (int)getpid(); + fprintf(stderr, "[%d]pmXmitPDU: %s fd=%d len=%d", + mypid, __pmPDUTypeStr_r(php->type, strbuf, sizeof(strbuf)), fd, php->len); + for (j = 0; j < jend; j++) { + if ((j % 8) == 0) + fprintf(stderr, "\n%03d: ", j); + fprintf(stderr, "%8x ", pdubuf[j]); + } + putc('\n', stderr); + } +#endif + len = php->len; + + php->len = htonl(php->len); + php->from = htonl(php->from); + php->type = htonl(php->type); + while (off < len) { + char *p = (char *)pdubuf; + int n; + + p += off; + + n = socketipc ? __pmSend(fd, p, len-off, 0) : write(fd, p, len-off); + if (n < 0) + break; + off += n; + } + php->len = ntohl(php->len); + php->from = ntohl(php->from); + php->type = ntohl(php->type); + + if (off != len) { + if (socketipc) { + if (__pmSocketClosed()) + return PM_ERR_IPC; + return neterror() ? -neterror() : PM_ERR_IPC; + } + return oserror() ? -oserror() : PM_ERR_IPC; + } + + __pmOverrideLastFd(fd); + if (php->type >= PDU_START && php->type <= PDU_FINISH) + __pmPDUCntOut[php->type-PDU_START]++; + + return off; +} + +/* result is pinned on successful return */ +int +__pmGetPDU(int fd, int mode, int timeout, __pmPDU **result) +{ + int need; + int len; + static int maxsize = PDU_CHUNK; + char *handle; + __pmPDU *pdubuf; + __pmPDU *pdubuf_prev; + __pmPDUHdr *php; + + if ((pdubuf = __pmFindPDUBuf(maxsize)) == NULL) + return -oserror(); + + /* First read - try to read the header */ + len = pduread(fd, (void *)pdubuf, sizeof(__pmPDUHdr), HEADER, timeout); + php = (__pmPDUHdr *)pdubuf; + + if (len < (int)sizeof(__pmPDUHdr)) { + if (len == -1) { + if (__pmSocketClosed()) { + len = 0; + } else { + char errmsg[PM_MAXERRMSGLEN]; + __pmNotifyErr(LOG_ERR, "__pmGetPDU: fd=%d hdr read: len=%d: %s", fd, len, pmErrStr_r(-oserror(), errmsg, sizeof(errmsg))); + } + } + else if (len >= (int)sizeof(php->len)) { + /* + * Have part of a PDU header. Enough for the "len" + * field to be valid, but not yet all of it - save + * what we have received and try to read some more. + * Note this can only happen once per PDU, so the + * ntohl() below will _only_ be done once per PDU. + */ + goto check_read_len; /* continue, do not return */ + } + else if (len == PM_ERR_TIMEOUT) { + __pmUnpinPDUBuf(pdubuf); + return PM_ERR_TIMEOUT; + } + else if (len < 0) { + char errmsg[PM_MAXERRMSGLEN]; + __pmNotifyErr(LOG_ERR, "__pmGetPDU: fd=%d hdr read: len=%d: %s", fd, len, pmErrStr_r(len, errmsg, sizeof(errmsg))); + __pmUnpinPDUBuf(pdubuf); + return PM_ERR_IPC; + } + else if (len > 0) { + __pmNotifyErr(LOG_ERR, "__pmGetPDU: fd=%d hdr read: bad len=%d", fd, len); + __pmUnpinPDUBuf(pdubuf); + return PM_ERR_IPC; + } + + /* + * end-of-file with no data + */ + __pmUnpinPDUBuf(pdubuf); + return 0; + } + +check_read_len: + php->len = ntohl(php->len); + if (php->len < (int)sizeof(__pmPDUHdr)) { + /* + * PDU length indicates insufficient bytes for a PDU header + * ... looks like DOS attack like PV 935490 + */ + __pmNotifyErr(LOG_ERR, "__pmGetPDU: fd=%d illegal PDU len=%d in hdr", fd, php->len); + __pmUnpinPDUBuf(pdubuf); + return PM_ERR_IPC; + } + else if (mode == LIMIT_SIZE && php->len > ceiling) { + /* + * Guard against denial of service attack ... don't accept PDUs + * from clients that are larger than 64 Kbytes (ceiling) + * (note, pmcd and pmdas have to be able to _send_ large PDUs, + * e.g. for a pmResult or instance domain enquiry) + */ + __pmNotifyErr(LOG_ERR, "__pmGetPDU: fd=%d bad PDU len=%d in hdr exceeds maximum client PDU size (%d)", + fd, php->len, ceiling); + + __pmUnpinPDUBuf(pdubuf); + return PM_ERR_TOOBIG; + } + + if (len < php->len) { + /* + * need to read more ... + */ + int tmpsize; + int have = len; + + PM_INIT_LOCKS(); + PM_LOCK(__pmLock_libpcp); + if (php->len > maxsize) { + tmpsize = PDU_CHUNK * ( 1 + php->len / PDU_CHUNK); + maxsize = tmpsize; + } + else + tmpsize = maxsize; + PM_UNLOCK(__pmLock_libpcp); + + pdubuf_prev = pdubuf; + if ((pdubuf = __pmFindPDUBuf(tmpsize)) == NULL) { + __pmUnpinPDUBuf(pdubuf_prev); + return -oserror(); + } + + memmove((void *)pdubuf, (void *)php, len); + __pmUnpinPDUBuf(pdubuf_prev); + + php = (__pmPDUHdr *)pdubuf; + need = php->len - have; + handle = (char *)pdubuf; + /* block until all of the PDU is received this time */ + len = pduread(fd, (void *)&handle[len], need, BODY, timeout); + if (len != need) { + if (len == PM_ERR_TIMEOUT) { + __pmUnpinPDUBuf(pdubuf); + return PM_ERR_TIMEOUT; + } + else if (len < 0) { + char errmsg[PM_MAXERRMSGLEN]; + __pmNotifyErr(LOG_ERR, "__pmGetPDU: fd=%d data read: len=%d: %s", fd, len, pmErrStr_r(-oserror(), errmsg, sizeof(errmsg))); + } + else + __pmNotifyErr(LOG_ERR, "__pmGetPDU: fd=%d data read: have %d, want %d, got %d", fd, have, need, len); + /* + * only report header fields if you've read enough bytes + */ + if (len > 0) + have += len; + if (have >= (int)(sizeof(php->len)+sizeof(php->type)+sizeof(php->from))) + __pmNotifyErr(LOG_ERR, "__pmGetPDU: PDU hdr: len=0x%x type=0x%x from=0x%x", php->len, (unsigned)ntohl(php->type), (unsigned)ntohl(php->from)); + else if (have >= (int)(sizeof(php->len)+sizeof(php->type))) + __pmNotifyErr(LOG_ERR, "__pmGetPDU: PDU hdr: len=0x%x type=0x%x", php->len, (unsigned)ntohl(php->type)); + __pmUnpinPDUBuf(pdubuf); + return PM_ERR_IPC; + } + } + + *result = (__pmPDU *)php; + php->type = ntohl((unsigned int)php->type); + if (php->type < 0) { + /* + * PDU type is bad ... could be a possible mem leak attack like + * https://bugzilla.redhat.com/show_bug.cgi?id=841319 + */ + __pmNotifyErr(LOG_ERR, "__pmGetPDU: fd=%d illegal PDU type=%d in hdr", fd, php->type); + __pmUnpinPDUBuf(pdubuf); + return PM_ERR_IPC; + } + php->from = ntohl((unsigned int)php->from); +#ifdef PCP_DEBUG + if (pmDebug & DBG_TRACE_PDU) { + int j; + char *p; + int jend = PM_PDU_SIZE(php->len); + char strbuf[20]; + + /* for Purify ... */ + p = (char *)*result + php->len; + while (p < (char *)*result + jend*sizeof(__pmPDU)) + *p++ = '~'; /* buffer end */ + + if (mypid == -1) + mypid = (int)getpid(); + fprintf(stderr, "[%d]pmGetPDU: %s fd=%d len=%d from=%d", + mypid, __pmPDUTypeStr_r(php->type, strbuf, sizeof(strbuf)), fd, php->len, php->from); + for (j = 0; j < jend; j++) { + if ((j % 8) == 0) + fprintf(stderr, "\n%03d: ", j); + fprintf(stderr, "%8x ", (*result)[j]); + } + putc('\n', stderr); + } +#endif + if (php->type >= PDU_START && php->type <= PDU_FINISH) + __pmPDUCntIn[php->type-PDU_START]++; + + /* + * Note php points into the PDU buffer pdubuf that remains pinned + * and php is returned via the result parameter ... see the + * thread-safe comments above + */ + return php->type; +} + +int +__pmGetPDUCeiling(void) +{ + return ceiling; +} + +int +__pmSetPDUCeiling(int newceiling) +{ + if (newceiling > 0) + return (ceiling = newceiling); + return ceiling; +} + +void +__pmSetPDUCntBuf(unsigned *in, unsigned *out) +{ + __pmPDUCntIn = in; + __pmPDUCntOut = out; +} |