Commit message: core: fix potential misadressing in parser message sanitizer

misadressing could happen when an oversize message made it to the
sanitizer AND contained a control character in the oversize part
of the message. Note that it is an error in itself that such an
oversize message enters the system, but we harden the sanitizer
to handle this gracefully (it will truncate the message).

Note that truncation may still - as previously - happen if the
number of escape characters makes the string grow above the max
message size.
CVE: None
Vulnerability description: None

Project: rsyslog
File: parser.c

ParsePRI(smsg_t *pMsg)
{
	syslog_pri_t pri;
	uchar *msg;
	int lenMsg;
	DEFiRet;

	/* pull PRI */
	lenMsg = pMsg->iLenRawMsg;
	msg = pMsg->pszRawMsg;
	pri = DEFUPRI;
	if(pMsg->msgFlags & NO_PRI_IN_RAW) {
		/* In this case, simply do so as if the pri would be right at top */
		MsgSetAfterPRIOffs(pMsg, 0);
	} else {
		if(*msg == '<') {
			pri = 0;
			while(--lenMsg > 0 && isdigit((int) *++msg) && pri <= LOG_MAXPRI) {
				pri = 10 * pri + (*msg - '0');
			}
			if(*msg == '>') {
				++msg;
			} else {
				pri = LOG_PRI_INVLD;
			}
			if(pri > LOG_MAXPRI)
				pri = LOG_PRI_INVLD;
		}
		msgSetPRI(pMsg, pri);
		MsgSetAfterPRIOffs(pMsg, (pri == LOG_PRI_INVLD) ? 0 : msg - pMsg->pszRawMsg);
	}
	RETiRet;
}

Project: rsyslog
File: parser.c

	if(pIf->ifVersion != parserCURR_IF_VERSION) { /* check for current version, increment on each change */
		ABORT_FINALIZE(RS_RET_INTERFACE_NOT_SUPPORTED);
	}

Project: rsyslog
File: parser.c

static rsRetVal uncompressMessage(smsg_t *pMsg)
{
	DEFiRet;
	uchar *deflateBuf = NULL;
	uLongf iLenDefBuf;
	uchar *pszMsg;
	size_t lenMsg;
	
	assert(pMsg != NULL);
	pszMsg = pMsg->pszRawMsg;
	lenMsg = pMsg->iLenRawMsg;

	/* we first need to check if we have a compressed record. If so,
	 * we must decompress it.
	 */
	if(lenMsg > 0 && *pszMsg == 'z') { /* compressed data present? (do NOT change order if conditions!) */
		/* we have compressed data, so let's deflate it. We support a maximum
		 * message size of iMaxLine. If it is larger, an error message is logged
		 * and the message is dropped. We do NOT try to decompress larger messages
		 * as such might be used for denial of service. It might happen to later
		 * builds that such functionality be added as an optional, operator-configurable
		 * feature.
		 */
		int ret;
		iLenDefBuf = glbl.GetMaxLine();
		CHKmalloc(deflateBuf = MALLOC(iLenDefBuf + 1));
		ret = uncompress((uchar *) deflateBuf, &iLenDefBuf, (uchar *) pszMsg+1, lenMsg-1);
		DBGPRINTF("Compressed message uncompressed with status %d, length: new %ld, old %d.\n",
		        ret, (long) iLenDefBuf, (int) (lenMsg-1));
		/* Now check if the uncompression worked. If not, there is not much we can do. In
		 * that case, we log an error message but ignore the message itself. Storing the
		 * compressed text is dangerous, as it contains control characters. So we do
		 * not do this. If someone would like to have a copy, this code here could be
		 * modified to do a hex-dump of the buffer in question. We do not include
		 * this functionality right now.
		 * rgerhards, 2006-12-07
		 */
		if(ret != Z_OK) {
			errmsg.LogError(0, NO_ERRCODE, "Uncompression of a message failed with return code %d "
			            "- enable debug logging if you need further information. "
				    "Message ignored.", ret);
			FINALIZE; /* unconditional exit, nothing left to do... */
		}
		MsgSetRawMsg(pMsg, (char*)deflateBuf, iLenDefBuf);
	}
finalize_it:
	if(deflateBuf != NULL)
		free(deflateBuf);

	RETiRet;
}

Project: rsyslog
File: parser.c

printParserList(parserList_t *pList)
{
	while(pList != NULL) {
		dbgprintf("parser: %s\n", pList->pParser->pName);
		pList = pList->pNext;
	}
}

Project: rsyslog
File: parser.c

static rsRetVal parserConstructFinalize(parser_t *pThis)
{
	DEFiRet;

	ISOBJ_TYPE_assert(pThis, parser);
	CHKiRet(AddParserToList(&pParsLstRoot, pThis));
	DBGPRINTF("Parser '%s' added to list of available parsers.\n", pThis->pName);

finalize_it:
	RETiRet;
}

Project: rsyslog
File: parser.c

ParseMsg(smsg_t *pMsg)
{
	rsRetVal localRet = RS_RET_ERR;
	parserList_t *pParserList;
	parser_t *pParser;
	sbool bIsSanitized;
	sbool bPRIisParsed;
	static int iErrMsgRateLimiter = 0;
	DEFiRet;

	if(pMsg->iLenRawMsg == 0)
		ABORT_FINALIZE(RS_RET_EMPTY_MSG);

	CHKiRet(uncompressMessage(pMsg));

	/* we take the risk to print a non-sanitized string, because this is the best we can get
	 * (and that functionality is too important for debugging to drop it...).
	 */
	DBGPRINTF("msg parser: flags %x, from '%s', msg '%.60s'\n", pMsg->msgFlags,
		  (pMsg->msgFlags & NEEDS_DNSRESOL) ? UCHAR_CONSTANT("~NOTRESOLVED~") : getRcvFrom(pMsg),
		  pMsg->pszRawMsg);

	/* we now need to go through our list of parsers and see which one is capable of
	 * parsing the message. Note that the first parser that requires message sanitization
	 * will cause it to happen. After that, access to the unsanitized message is no
	 * loger possible.
	 */
	pParserList = ruleset.GetParserList(ourConf, pMsg);
	if(pParserList == NULL) {
		pParserList = pDfltParsLst;
	}
	DBGPRINTF("parse using parser list %p%s.\n", pParserList,
		  (pParserList == pDfltParsLst) ? " (the default list)" : "");

	bIsSanitized = RSFALSE;
	bPRIisParsed = RSFALSE;
	while(pParserList != NULL) {
		pParser = pParserList->pParser;
		if(pParser->bDoSanitazion && bIsSanitized == RSFALSE) {
			CHKiRet(SanitizeMsg(pMsg));
			if(pParser->bDoPRIParsing && bPRIisParsed == RSFALSE) {
				CHKiRet(ParsePRI(pMsg));
				bPRIisParsed = RSTRUE;
			}
			bIsSanitized = RSTRUE;
		}
		if(pParser->pModule->mod.pm.parse2 == NULL)
			localRet = pParser->pModule->mod.pm.parse(pMsg);
		else
			localRet = pParser->pModule->mod.pm.parse2(pParser->pInst, pMsg);
		DBGPRINTF("Parser '%s' returned %d\n", pParser->pName, localRet);
		if(localRet != RS_RET_COULD_NOT_PARSE)
			break;
		pParserList = pParserList->pNext;
	}

	/* We need to log a warning message and drop the message if we did not find a parser.
	 * Note that we log at most the first 1000 message, as this may very well be a problem
	 * that causes a message generation loop. We do not synchronize that counter, it doesn't
	 * matter if we log a handful messages more than we should...
	 */
	if(localRet != RS_RET_OK) {
		if(++iErrMsgRateLimiter > 1000) {
			errmsg.LogError(0, localRet, "Error: one message could not be processed by "
			 	"any parser, message is being discarded (start of raw msg: '%.50s')", 
				pMsg->pszRawMsg);
		}
		DBGPRINTF("No parser could process the message (state %d), we need to discard it.\n", localRet);
		ABORT_FINALIZE(localRet);
	}

	/* "finalize" message object */
	pMsg->msgFlags &= ~NEEDS_PARSING; /* this message is now parsed */

finalize_it:
	RETiRet;
}

Project: rsyslog
File: parser.c

destroyMasterParserList(void)
{
	parserList_t *pParsLst;
	parserList_t *pParsLstDel;

	pParsLst = pParsLstRoot;
	while(pParsLst != NULL) {
		parserDestruct(&pParsLst->pParser);
		pParsLstDel = pParsLst;
		pParsLst = pParsLst->pNext;
		free(pParsLstDel);
	}
}

Project: rsyslog
File: parser.c

FindParser(parser_t **ppParser, uchar *pName)
{
	parserList_t *pThis;
	DEFiRet;
	
	for(pThis = pParsLstRoot ; pThis != NULL ; pThis = pThis->pNext) {
		if(ustrcmp(pThis->pParser->pName, pName) == 0) {
			*ppParser = pThis->pParser;
			FINALIZE;	/* found it, iRet still eq. OK! */
		}
	}

	iRet = RS_RET_PARSER_NOT_FOUND;

finalize_it:
	RETiRet;
}

Project: rsyslog
File: parser.c

SetName(parser_t *pThis, uchar *name)
{
	DEFiRet;

	ISOBJ_TYPE_assert(pThis, parser);
	assert(name != NULL);

	if(pThis->pName != NULL) {
		free(pThis->pName);
		pThis->pName = NULL;
	}

	CHKmalloc(pThis->pName = ustrdup(name));

finalize_it:
	RETiRet;
}

Project: rsyslog
File: parser.c

SetModPtr(parser_t *pThis, modInfo_t *pMod)
{
	ISOBJ_TYPE_assert(pThis, parser);
	assert(pMod != NULL);
	assert(pThis->pModule == NULL);
	pThis->pModule = pMod;
	return RS_RET_OK;
}

Project: rsyslog
File: parser.c

SanitizeMsg(smsg_t *pMsg)
{
	DEFiRet;
	uchar *pszMsg;
	uchar *pDst; /* destination for copy job */
	size_t lenMsg;
	size_t iSrc;
	size_t iDst;
	size_t iMaxLine;
	size_t maxDest;
	uchar pc;
	sbool bUpdatedLen = RSFALSE;
	uchar szSanBuf[32*1024]; /* buffer used for sanitizing a string */

	assert(pMsg != NULL);
	assert(pMsg->iLenRawMsg > 0);

	pszMsg = pMsg->pszRawMsg;
	lenMsg = pMsg->iLenRawMsg;

	/* remove NUL character at end of message (see comment in function header)
	 * Note that we do not need to add a NUL character in this case, because it
	 * is already present ;)
	 */
	if(pszMsg[lenMsg-1] == '\0') {
		DBGPRINTF("dropped NUL at very end of message\n");
		bUpdatedLen = RSTRUE;
		lenMsg--;
	}

	/* then we check if we need to drop trailing LFs, which often make
	 * their way into syslog messages unintentionally. In order to remain
	 * compatible to recent IETF developments, we allow the user to
	 * turn on/off this handling.  rgerhards, 2007-07-23
	 */
	if(glbl.GetParserDropTrailingLFOnReception()
	   && lenMsg > 0 && pszMsg[lenMsg-1] == '\n') {
		DBGPRINTF("dropped LF at very end of message (DropTrailingLF is set)\n");
		lenMsg--;
		pszMsg[lenMsg] = '\0';
		bUpdatedLen = RSTRUE;
	}

	/* it is much quicker to sweep over the message and see if it actually
	 * needs sanitation than to do the sanitation in any case. So we first do
	 * this and terminate when it is not needed - which is expectedly the case
	 * for the vast majority of messages. -- rgerhards, 2009-06-15
	 * Note that we do NOT check here if tab characters are to be escaped or
	 * not. I expect this functionality to be seldomly used and thus I do not
	 * like to pay the performance penalty. So the penalty is only with those
	 * that actually use it, because we may call the sanitizer without actual
	 * need below (but it then still will work perfectly well!). -- rgerhards, 2009-11-27
	 */
	int bNeedSanitize = 0;
	for(iSrc = 0 ; iSrc < lenMsg ; iSrc++) {
		if(pszMsg[iSrc] < 32) {
			if(glbl.GetParserSpaceLFOnReceive() && pszMsg[iSrc] == '\n') {
				pszMsg[iSrc] = ' ';
			} else if(pszMsg[iSrc] == '\0' || glbl.GetParserEscapeControlCharactersOnReceive()) {
				bNeedSanitize = 1;
				if (!glbl.GetParserSpaceLFOnReceive()) {
					break;
			    }
			}
		} else if(pszMsg[iSrc] > 127 && glbl.GetParserEscape8BitCharactersOnReceive()) {
			bNeedSanitize = 1;
			break;
		}
	}

	if(!bNeedSanitize) {
		if(bUpdatedLen == RSTRUE)
			MsgSetRawMsgSize(pMsg, lenMsg);
		FINALIZE;
	}

	/* now copy over the message and sanitize it. Note that up to iSrc-1 there was
	 * obviously no need to sanitize, so we can go over that quickly...
	 */
	iMaxLine = glbl.GetMaxLine();
	maxDest = lenMsg * 4; /* message can grow at most four-fold */

	if(maxDest > iMaxLine)
		maxDest = iMaxLine;	/* but not more than the max size! */
	if(maxDest < sizeof(szSanBuf))
		pDst = szSanBuf;
	else 
		CHKmalloc(pDst = MALLOC(maxDest + 1));
	if(iSrc > 0) {
		iSrc--; /* go back to where everything is OK */
		if(iSrc > maxDest) {
			DBGPRINTF("parser.Sanitize: have oversize index %zd, "
				"max %zd - corrected, but should not happen\n",
				iSrc, maxDest);
			iSrc = maxDest;
		}
		memcpy(pDst, pszMsg, iSrc); /* fast copy known good */
	}
	iDst = iSrc;
	while(iSrc < lenMsg && iDst < maxDest - 3) { /* leave some space if last char must be escaped */
		if((pszMsg[iSrc] < 32) && (pszMsg[iSrc] != '\t' || glbl.GetParserEscapeControlCharacterTab())) {
			/* note: \0 must always be escaped, the rest of the code currently
			 * can not handle it! -- rgerhards, 2009-08-26
			 */
			if(pszMsg[iSrc] == '\0' || glbl.GetParserEscapeControlCharactersOnReceive()) {
				/* we are configured to escape control characters. Please note
				 * that this most probably break non-western character sets like
				 * Japanese, Korean or Chinese. rgerhards, 2007-07-17
				 */
				if (glbl.GetParserEscapeControlCharactersCStyle()) {
					pDst[iDst++] = '\\';

					switch (pszMsg[iSrc]) {
					case '\0':
						pDst[iDst++] = '0';
						break;
					case '\a':
						pDst[iDst++] = 'a';
						break;
					case '\b':
						pDst[iDst++] = 'b';
						break;
					case '\e':
						pDst[iDst++] = 'e';
						break;
					case '\f':
						pDst[iDst++] = 'f';
						break;
					case '\n':
						pDst[iDst++] = 'n';
						break;
					case '\r':
						pDst[iDst++] = 'r';
						break;
					case '\t':
						pDst[iDst++] = 't';
						break;
					case '\v':
						pDst[iDst++] = 'v';
						break;
					default:
						pDst[iDst++] = 'x';

						pc = pszMsg[iSrc];
						pDst[iDst++] = hexdigit[(pc & 0xF0) >> 4];
						pDst[iDst++] = hexdigit[pc & 0xF];

						break;
					}

				} else {
					pDst[iDst++] = glbl.GetParserControlCharacterEscapePrefix();
					pDst[iDst++] = '0' + ((pszMsg[iSrc] & 0300) >> 6);
					pDst[iDst++] = '0' + ((pszMsg[iSrc] & 0070) >> 3);
					pDst[iDst++] = '0' + ((pszMsg[iSrc] & 0007));
				}
			}

		} else if(pszMsg[iSrc] > 127 && glbl.GetParserEscape8BitCharactersOnReceive()) {
			if (glbl.GetParserEscapeControlCharactersCStyle()) {
				pDst[iDst++] = '\\';
				pDst[iDst++] = 'x';

				pc = pszMsg[iSrc];
				pDst[iDst++] = hexdigit[(pc & 0xF0) >> 4];
				pDst[iDst++] = hexdigit[pc & 0xF];

			} else {
				/* In this case, we also do the conversion. Note that this most
				 * probably breaks European languages. -- rgerhards, 2010-01-27
				 */
				pDst[iDst++] = glbl.GetParserControlCharacterEscapePrefix();
				pDst[iDst++] = '0' + ((pszMsg[iSrc] & 0300) >> 6);
				pDst[iDst++] = '0' + ((pszMsg[iSrc] & 0070) >> 3);
				pDst[iDst++] = '0' + ((pszMsg[iSrc] & 0007));
			}
		} else {
			pDst[iDst++] = pszMsg[iSrc];
		}
		++iSrc;
	}
	pDst[iDst] = '\0';

	MsgSetRawMsg(pMsg, (char*)pDst, iDst); /* save sanitized string */

	if(pDst != szSanBuf)
		free(pDst);

finalize_it:
	RETiRet;
}

Project: rsyslog
File: parser.c

parserConstructViaModAndName(modInfo_t *__restrict__ pMod, uchar *const __restrict__ pName, void *pInst)
{
	rsRetVal localRet;
	parser_t *pParser = NULL;
	DEFiRet;

	if(pInst == NULL && pMod->mod.pm.newParserInst != NULL) {
		/* this happens for the default instance on ModLoad time */
		CHKiRet(pMod->mod.pm.newParserInst(NULL, &pInst));
	}
	CHKiRet(parserConstruct(&pParser));
	/* check some features */
	localRet = pMod->isCompatibleWithFeature(sFEATUREAutomaticSanitazion);
	if(localRet == RS_RET_OK){
		pParser->bDoSanitazion = RSTRUE;
	}
	localRet = pMod->isCompatibleWithFeature(sFEATUREAutomaticPRIParsing);
	if(localRet == RS_RET_OK){
		CHKiRet(SetDoPRIParsing(pParser, RSTRUE));
	}

	CHKiRet(SetName(pParser, pName));
	CHKiRet(SetModPtr(pParser, pMod));
	pParser->pInst = pInst;
	CHKiRet(parserConstructFinalize(pParser));
finalize_it:
	if(iRet != RS_RET_OK)
		free(pParser);
	RETiRet;
}

Project: rsyslog
File: parser.c

	if(pThis->pInst != NULL) {
		pThis->pModule->mod.pm.freeParserInst(pThis->pInst);
	}

Project: rsyslog
File: parser.c

AddParserToList(parserList_t **ppListRoot, parser_t *pParser)
{
	parserList_t *pThis;
	parserList_t *pTail;
	DEFiRet;

	CHKmalloc(pThis = MALLOC(sizeof(parserList_t)));
	pThis->pParser = pParser;
	pThis->pNext = NULL;

	if(*ppListRoot == NULL) {
		pThis->pNext = *ppListRoot;
		*ppListRoot = pThis;
	} else {
		/* find tail first */
		for(pTail = *ppListRoot ; pTail->pNext != NULL ; pTail = pTail->pNext)
			/* just search, do nothing else */;
		/* add at tail */
		pTail->pNext = pThis;
	}
DBGPRINTF("DDDDD: added parser '%s' to list %p\n", pParser->pName, ppListRoot);
finalize_it:
	RETiRet;
}

Project: rsyslog
File: parser.c

InitParserList(parserList_t **pListRoot)
{
	*pListRoot = NULL;
	return RS_RET_OK;
}

Project: rsyslog
File: parser.c

DestructParserList(parserList_t **ppListRoot)
{
	parserList_t *pParsLst;
	parserList_t *pParsLstDel;

	pParsLst = *ppListRoot;
	while(pParsLst != NULL) {
		pParsLstDel = pParsLst;
		pParsLst = pParsLst->pNext;
		free(pParsLstDel);
	}
	*ppListRoot = NULL;
	return RS_RET_OK;
}

Project: rsyslog
File: parser.c

SetDoPRIParsing(parser_t *pThis, int bDoIt)
{
	ISOBJ_TYPE_assert(pThis, parser);
	pThis->bDoPRIParsing = bDoIt;
	return RS_RET_OK;
}

Project: rsyslog
File: parser.c

AddDfltParser(uchar *pName)
{
	parser_t *pParser;
	DEFiRet;

	CHKiRet(FindParser(&pParser, pName));
	CHKiRet(AddParserToList(&pDfltParsLst, pParser));
	DBGPRINTF("Parser '%s' added to default parser set.\n", pName);
	
finalize_it:
	RETiRet;
}