QCborStreamReader: avoid allocating result if data is insufficient

By calling the internal readStringChunk() function with a QByteArray
pointer, QCborStreamReader::readByteArray() can now avoid allocating the
resulting buffer until the internals have confirmed that there is
sufficient data in the incoming buffer. As a result, we first detect the
EOF condition before we conclude the payload would have been too big for
QByteArray (validation()) test. Meanwhile, the hugeDeviceValidation()
test ends up with a few conditions where it would have copied 1 GB of
data, so limit that too.

We make a choice of reporting OOM vs DataTooLarge only if QByteArray
fails to allocate in the first place (QByteArray::resize() ->
Q_CHECK_PTR -> qBadAlloc, QtCore is always built with exceptions on).

The QCborValue unit test needed a temporary work around until we apply
the same allocation fix (see next commit).

Pick-to: 5.15 6.0
Fixes: QTBUG-88253
Change-Id: I7b9b97ae9b32412abdc6fffd164523eeae49cdfe
Reviewed-by: Allan Sandfeld Jensen <allan.jensen@qt.io>
This commit is contained in:
Thiago Macieira 2020-11-11 12:50:54 -08:00 committed by Volker Hilsheimer
parent c16ad16bd0
commit 9a55f40937
4 changed files with 70 additions and 29 deletions

View File

@ -667,8 +667,22 @@ public:
bufferStart = 0; bufferStart = 0;
} }
struct ReadStringChunk {
union {
char *ptr;
QByteArray *array;
};
enum { ByteArray = -1 };
qsizetype maxlen_or_type;
ReadStringChunk(char *ptr, qsizetype maxlen) : ptr(ptr), maxlen_or_type(maxlen) {}
ReadStringChunk(QByteArray *array) : array(array), maxlen_or_type(ByteArray) {}
bool isByteArray() const { return maxlen_or_type == ByteArray; }
bool isPlainPointer() const { return maxlen_or_type >= 0; }
};
QCborStreamReader::StringResult<qsizetype> readStringChunk(ReadStringChunk params);
bool ensureStringIteration(); bool ensureStringIteration();
QCborStreamReader::StringResult<qsizetype> readStringChunk(char *ptr, qsizetype maxlen);
}; };
void qt_cbor_stream_set_error(QCborStreamReaderPrivate *d, QCborError error) void qt_cbor_stream_set_error(QCborStreamReaderPrivate *d, QCborError error)
@ -1366,7 +1380,7 @@ QCborStreamReader::StringResult<QString> QCborStreamReader::_readString_helper()
} }
/*! /*!
\fn QCborStreamReader::StringResult<QString> QCborStreamReader::readByteArray() \fn QCborStreamReader::StringResult<QByteArray> QCborStreamReader::readByteArray()
Decodes one byte array chunk from the CBOR string and returns it. This Decodes one byte array chunk from the CBOR string and returns it. This
function is used for both regular and chunked contents, so the caller must function is used for both regular and chunked contents, so the caller must
@ -1384,19 +1398,16 @@ QCborStreamReader::StringResult<QString> QCborStreamReader::_readString_helper()
QCborStreamReader::StringResult<QByteArray> QCborStreamReader::_readByteArray_helper() QCborStreamReader::StringResult<QByteArray> QCborStreamReader::_readByteArray_helper()
{ {
QCborStreamReader::StringResult<QByteArray> result; QCborStreamReader::StringResult<QByteArray> result;
result.status = Error; auto r = d->readStringChunk(&result.data);
qsizetype len = _currentStringChunkSize(); result.status = r.status;
if (len < 0) if (r.status == Error) {
return result; result.data.clear();
if (len > MaxByteArraySize) { } else {
d->handleError(CborErrorDataTooLarge); Q_ASSERT(r.data == result.data.length());
return result; if (r.status == EndOfString && lastError() == QCborError::NoError)
preparse();
} }
result.data.resize(len);
auto r = readStringChunk(result.data.data(), len);
Q_ASSERT(r.status != Ok || r.data == len);
result.status = r.status;
return result; return result;
} }
@ -1462,14 +1473,14 @@ qsizetype QCborStreamReader::_currentStringChunkSize() const
QCborStreamReader::StringResult<qsizetype> QCborStreamReader::StringResult<qsizetype>
QCborStreamReader::readStringChunk(char *ptr, qsizetype maxlen) QCborStreamReader::readStringChunk(char *ptr, qsizetype maxlen)
{ {
auto r = d->readStringChunk(ptr, maxlen); auto r = d->readStringChunk({ptr, maxlen});
if (r.status == EndOfString && lastError() == QCborError::NoError) if (r.status == EndOfString && lastError() == QCborError::NoError)
preparse(); preparse();
return r; return r;
} }
QCborStreamReader::StringResult<qsizetype> Q_NEVER_INLINE QCborStreamReader::StringResult<qsizetype>
QCborStreamReaderPrivate::readStringChunk(char *ptr, qsizetype maxlen) QCborStreamReaderPrivate::readStringChunk(ReadStringChunk params)
{ {
CborError err; CborError err;
size_t len; size_t len;
@ -1482,6 +1493,12 @@ QCborStreamReaderPrivate::readStringChunk(char *ptr, qsizetype maxlen)
if (!ensureStringIteration()) if (!ensureStringIteration())
return result; return result;
// Note: in the current implementation, the call into TinyCBOR below only
// succeeds if we *already* have all the data in memory. That's obvious for
// the case of direct memory (no QIODevice), whereas for QIODevices
// qt_cbor_decoder_transfer_string() enforces that
// QIODevice::bytesAvailable() be bigger than the amount we're about to
// read.
#if 1 #if 1
// Using internal TinyCBOR API! // Using internal TinyCBOR API!
err = _cbor_value_get_string_chunk(&currentElement, &content, &len, &currentElement); err = _cbor_value_get_string_chunk(&currentElement, &content, &len, &currentElement);
@ -1518,11 +1535,30 @@ QCborStreamReaderPrivate::readStringChunk(char *ptr, qsizetype maxlen)
qint64 actuallyRead; qint64 actuallyRead;
qptrdiff offset = qptrdiff(content); qptrdiff offset = qptrdiff(content);
qsizetype toRead = qsizetype(len); qsizetype toRead = qsizetype(len);
qsizetype left = toRead - maxlen; qsizetype left = 0; // bytes from the chunk not copied to the user buffer, to discard
if (left < 0) char *ptr;
left = 0; // buffer bigger than string
else if (params.isPlainPointer()) {
toRead = maxlen; // buffer smaller than string left = toRead - params.maxlen_or_type;
if (left < 0)
left = 0; // buffer bigger than string
else
toRead = params.maxlen_or_type; // buffer smaller than string
ptr = params.ptr;
} else if (params.isByteArray()) {
// See note above on having ensured there is enough incoming data.
try {
params.array->resize(toRead);
} catch (const std::bad_alloc &) {
// the distinction between DataTooLarge and OOM is mostly for
// compatibility with Qt 5; in Qt 6, we could consider everything
// to be OOM.
handleError(toRead > MaxByteArraySize ? CborErrorDataTooLarge: CborErrorOutOfMemory);
return result;
}
ptr = const_cast<char *>(params.array->constData());
}
if (device) { if (device) {
// This first skip can't fail because we've already read this many bytes. // This first skip can't fail because we've already read this many bytes.

View File

@ -67,7 +67,7 @@ qint64 LargeIODevice::readData(char *data, qint64 maxlen)
qint64 p = pos(); qint64 p = pos();
if (maxlen > realSize - p) if (maxlen > realSize - p)
maxlen = realSize - p; maxlen = realSize - p;
memset(data, '\0', maxlen); memset(data, '\0', qMin(maxlen, qint64(32 * 1024)));
qint64 fromstart = start.size() - p; qint64 fromstart = start.size() - p;
if (fromstart > maxlen) if (fromstart > maxlen)
@ -88,24 +88,24 @@ void addValidationLargeData(qsizetype minInvalid, qsizetype maxInvalid)
qToBigEndian(v, toolong + 1); qToBigEndian(v, toolong + 1);
QTest::addRow("bytearray-too-big-for-qbytearray-%llx", v) QTest::addRow("bytearray-too-big-for-qbytearray-%llx", v)
<< QByteArray(toolong, sizeof(toolong)) << 0 << CborErrorDataTooLarge; << QByteArray(toolong, sizeof(toolong)) << 0 << CborErrorUnexpectedEOF;
QTest::addRow("bytearray-chunked-too-big-for-qbytearray-%llx", v) QTest::addRow("bytearray-chunked-too-big-for-qbytearray-%llx", v)
<< ('\x5f' + QByteArray(toolong, sizeof(toolong)) + '\xff') << ('\x5f' + QByteArray(toolong, sizeof(toolong)) + '\xff')
<< 0 << CborErrorDataTooLarge; << 0 << CborErrorUnexpectedEOF;
QTest::addRow("bytearray-2chunked-too-big-for-qbytearray-%llx", v) QTest::addRow("bytearray-2chunked-too-big-for-qbytearray-%llx", v)
<< ("\x5f\x40" + QByteArray(toolong, sizeof(toolong)) + '\xff') << ("\x5f\x40" + QByteArray(toolong, sizeof(toolong)) + '\xff')
<< 0 << CborErrorDataTooLarge; << 0 << CborErrorUnexpectedEOF;
toolong[0] |= 0x20; toolong[0] |= 0x20;
// QCborStreamReader::readString copies to a QByteArray first // QCborStreamReader::readString copies to a QByteArray first
QTest::addRow("string-too-big-for-qbytearray-%llx", v) QTest::addRow("string-too-big-for-qbytearray-%llx", v)
<< QByteArray(toolong, sizeof(toolong)) << 0 << CborErrorDataTooLarge; << QByteArray(toolong, sizeof(toolong)) << 0 << CborErrorUnexpectedEOF;
QTest::addRow("string-chunked-too-big-for-qbytearray-%llx", v) QTest::addRow("string-chunked-too-big-for-qbytearray-%llx", v)
<< ('\x7f' + QByteArray(toolong, sizeof(toolong)) + '\xff') << ('\x7f' + QByteArray(toolong, sizeof(toolong)) + '\xff')
<< 0 << CborErrorDataTooLarge; << 0 << CborErrorUnexpectedEOF;
QTest::addRow("string-2chunked-too-big-for-qbytearray-%llx", v) QTest::addRow("string-2chunked-too-big-for-qbytearray-%llx", v)
<< ("\x7f\x60" + QByteArray(toolong, sizeof(toolong)) + '\xff') << ("\x7f\x60" + QByteArray(toolong, sizeof(toolong)) + '\xff')
<< 0 << CborErrorDataTooLarge; << 0 << CborErrorUnexpectedEOF;
} }
} }

View File

@ -297,7 +297,10 @@ void tst_QCborStreamReader::integers()
void escapedAppendTo(QString &result, const QByteArray &data) void escapedAppendTo(QString &result, const QByteArray &data)
{ {
result += "h'" + QString::fromLatin1(data.toHex()) + '\''; QByteArray hex =
data.size() < 512*1024 ? data.toHex() :
"data of size " + QByteArray::number(data.size());
result += "h'" + QString::fromLatin1(hex) + '\'';
} }
void escapedAppendTo(QString &result, const QString &data) void escapedAppendTo(QString &result, const QString &data)

View File

@ -2042,6 +2042,7 @@ void tst_QCborValue::validation()
QCborParserError parserError; QCborParserError parserError;
QCborValue decoded = QCborValue::fromCbor(data, &parserError); QCborValue decoded = QCborValue::fromCbor(data, &parserError);
if (parserError.error != QCborError::DataTooLarge) // ### temporary!!
QCOMPARE(parserError.error, error); QCOMPARE(parserError.error, error);
if (data.startsWith('\x81')) { if (data.startsWith('\x81')) {
@ -2049,6 +2050,7 @@ void tst_QCborValue::validation()
char *ptr = const_cast<char *>(data.constData()); char *ptr = const_cast<char *>(data.constData());
QByteArray mid = QByteArray::fromRawData(ptr + 1, data.size() - 1); QByteArray mid = QByteArray::fromRawData(ptr + 1, data.size() - 1);
decoded = QCborValue::fromCbor(mid, &parserError); decoded = QCborValue::fromCbor(mid, &parserError);
if (parserError.error != QCborError::DataTooLarge) // ### temporary!!
QCOMPARE(parserError.error, error); QCOMPARE(parserError.error, error);
} }
} }