From ae2d88a65c69c27ea01478d5761fe385ac32a7f9 Mon Sep 17 00:00:00 2001 From: Jeff Sharkey Date: Sat, 26 Sep 2020 18:57:32 -0600 Subject: [PATCH 1/2] Rewrite of CursorWindow internals. The original CursorWindow implementation was created in Android 1.0 and has remained relatively unchanged since then. Unfortunately that design results in very poor performance on large windows, since reading or writing each FieldSlot is O(row/100) to traverse through a chain of RowSlotChunks. It's also memory-inefficient due to how it allocates RowSlotChunks in 404 byte chunks, even when there's only a single row to store. This change is a complete redesign of the CursorWindow internals to use a "heap-and-stack" style approach, where a "heap" of strings and blobs increment up from the bottom of the window while a "stack" of FieldSlots increment down from the top of the window. The included benchmarks show the following improvements, ensuring no regressions for small windows, while offering very dramatic improvements for larger windows: Big cores Little cores 4x4 cursor no regression no regression 1024x4 cursor 2.2x faster 2.0x faster 16384x4 cursor 48.5x faster 24.4x faster Detailed unit testing is also included to ensure that the rewrite behaves correctly. Bug: 169251528 Test: atest libandroidfw_tests Test: atest CtsDatabaseTestCases Test: atest FrameworksCoreTests:android.database Test: ./frameworks/base/libs/hwui/tests/scripts/prep_generic.sh little && atest libandroidfw_benchmarks Test: ./frameworks/base/libs/hwui/tests/scripts/prep_generic.sh little && atest CorePerfTests:android.database.CrossProcessCursorPerfTest Change-Id: I90dff31fd550130dae917a33e0e1fa684e15c107 --- libs/androidfw/Android.bp | 6 +- libs/androidfw/CursorWindow.cpp | 534 ++++++++---------- .../include/androidfw/CursorWindow.h | 141 ++--- libs/androidfw/tests/CursorWindow_bench.cpp | 86 +++ libs/androidfw/tests/CursorWindow_test.cpp | 359 ++++++++++++ 5 files changed, 749 insertions(+), 377 deletions(-) create mode 100644 libs/androidfw/tests/CursorWindow_bench.cpp create mode 100644 libs/androidfw/tests/CursorWindow_test.cpp diff --git a/libs/androidfw/Android.bp b/libs/androidfw/Android.bp index 8ab7da5257ab9..45ad94fdb75ac 100644 --- a/libs/androidfw/Android.bp +++ b/libs/androidfw/Android.bp @@ -155,11 +155,12 @@ cc_test { android: { srcs: [ "tests/BackupData_test.cpp", - "tests/BackupHelpers_test.cpp", + "tests/BackupHelpers_test.cpp", + "tests/CursorWindow_test.cpp", "tests/ObbFile_test.cpp", "tests/PosixUtils_test.cpp", ], - shared_libs: common_test_libs + ["libui"], + shared_libs: common_test_libs + ["libbinder", "liblog", "libui"], }, host: { static_libs: common_test_libs + ["liblog", "libz"], @@ -185,6 +186,7 @@ cc_benchmark { // Actual benchmarks. "tests/AssetManager2_bench.cpp", "tests/AttributeResolution_bench.cpp", + "tests/CursorWindow_bench.cpp", "tests/SparseEntry_bench.cpp", "tests/Theme_bench.cpp", ], diff --git a/libs/androidfw/CursorWindow.cpp b/libs/androidfw/CursorWindow.cpp index 71c8e1f6121f7..1e6de67a40ba8 100644 --- a/libs/androidfw/CursorWindow.cpp +++ b/libs/androidfw/CursorWindow.cpp @@ -14,19 +14,14 @@ * limitations under the License. */ -#undef LOG_TAG #define LOG_TAG "CursorWindow" #include -#include -#include -#include #include -#include -#include -#include +#include "android-base/logging.h" +#include "cutils/ashmem.h" namespace android { @@ -36,11 +31,10 @@ namespace android { */ static constexpr const size_t kInlineSize = 16384; -CursorWindow::CursorWindow(const String8& name, int ashmemFd, void* data, size_t size, - size_t inflatedSize, bool readOnly) : - mName(name), mAshmemFd(ashmemFd), mData(data), mSize(size), - mInflatedSize(inflatedSize), mReadOnly(readOnly) { - mHeader = static_cast(mData); +static constexpr const size_t kSlotShift = 4; +static constexpr const size_t kSlotSizeBytes = 1 << kSlotShift; + +CursorWindow::CursorWindow() { } CursorWindow::~CursorWindow() { @@ -52,234 +46,242 @@ CursorWindow::~CursorWindow() { } } -status_t CursorWindow::create(const String8& name, size_t inflatedSize, - CursorWindow** outCursorWindow) { - *outCursorWindow = nullptr; +status_t CursorWindow::create(const String8 &name, size_t inflatedSize, CursorWindow **outWindow) { + *outWindow = nullptr; - size_t size = std::min(kInlineSize, inflatedSize); - void* data = calloc(size, 1); - if (!data) return NO_MEMORY; + CursorWindow* window = new CursorWindow(); + if (!window) goto fail; - CursorWindow* window = new CursorWindow(name, -1, data, size, - inflatedSize, false /*readOnly*/); - status_t result = window->clear(); - if (!result) { - LOG_WINDOW("Created new CursorWindow: freeOffset=%d, " - "numRows=%d, numColumns=%d, mSize=%zu, mData=%p", - window->mHeader->freeOffset, - window->mHeader->numRows, - window->mHeader->numColumns, - window->mSize, window->mData); - *outCursorWindow = window; - return OK; - } + window->mName = name; + window->mSize = std::min(kInlineSize, inflatedSize); + window->mInflatedSize = inflatedSize; + window->mData = malloc(window->mSize); + if (!window->mData) goto fail; + window->mReadOnly = false; + + window->clear(); + window->updateSlotsData(); + + LOG(DEBUG) << "Created: " << window->toString(); + *outWindow = window; + return OK; + +fail: + LOG(ERROR) << "Failed create"; +fail_silent: delete window; - return result; + return UNKNOWN_ERROR; } -status_t CursorWindow::inflate() { - // Shortcut when we can't expand any further - if (mSize == mInflatedSize) return INVALID_OPERATION; +status_t CursorWindow::maybeInflate() { + int ashmemFd = 0; + void* newData = nullptr; + + // Bail early when we can't expand any further + if (mReadOnly || mSize == mInflatedSize) { + return INVALID_OPERATION; + } String8 ashmemName("CursorWindow: "); ashmemName.append(mName); - status_t result; - int ashmemFd = ashmem_create_region(ashmemName.string(), mInflatedSize); + ashmemFd = ashmem_create_region(ashmemName.string(), mInflatedSize); if (ashmemFd < 0) { - result = -errno; - ALOGE("CursorWindow: ashmem_create_region() failed: errno=%d.", errno); - } else { - result = ashmem_set_prot_region(ashmemFd, PROT_READ | PROT_WRITE); - if (result < 0) { - ALOGE("CursorWindow: ashmem_set_prot_region() failed: errno=%d",errno); - } else { - void* data = ::mmap(NULL, mInflatedSize, PROT_READ | PROT_WRITE, - MAP_SHARED, ashmemFd, 0); - if (data == MAP_FAILED) { - result = -errno; - ALOGE("CursorWindow: mmap() failed: errno=%d.", errno); - } else { - result = ashmem_set_prot_region(ashmemFd, PROT_READ); - if (result < 0) { - ALOGE("CursorWindow: ashmem_set_prot_region() failed: errno=%d.", errno); - } else { - // Move inline contents into new ashmem region - memcpy(data, mData, mSize); - free(mData); - mAshmemFd = ashmemFd; - mData = data; - mHeader = static_cast(mData); - mSize = mInflatedSize; - LOG_WINDOW("Inflated CursorWindow: freeOffset=%d, " - "numRows=%d, numColumns=%d, mSize=%zu, mData=%p", - mHeader->freeOffset, - mHeader->numRows, - mHeader->numColumns, - mSize, mData); - return OK; - } - } - ::munmap(data, mInflatedSize); - } - ::close(ashmemFd); + PLOG(ERROR) << "Failed ashmem_create_region"; + goto fail_silent; } - return result; + + if (ashmem_set_prot_region(ashmemFd, PROT_READ | PROT_WRITE) < 0) { + PLOG(ERROR) << "Failed ashmem_set_prot_region"; + goto fail_silent; + } + + newData = ::mmap(nullptr, mInflatedSize, PROT_READ | PROT_WRITE, MAP_SHARED, ashmemFd, 0); + if (newData == MAP_FAILED) { + PLOG(ERROR) << "Failed mmap"; + goto fail_silent; + } + + if (ashmem_set_prot_region(ashmemFd, PROT_READ) < 0) { + PLOG(ERROR) << "Failed ashmem_set_prot_region"; + goto fail_silent; + } + + { + // Migrate existing contents into new ashmem region + uint32_t slotsSize = mSize - mSlotsOffset; + uint32_t newSlotsOffset = mInflatedSize - slotsSize; + memcpy(static_cast(newData), + static_cast(mData), mAllocOffset); + memcpy(static_cast(newData) + newSlotsOffset, + static_cast(mData) + mSlotsOffset, slotsSize); + + free(mData); + mAshmemFd = ashmemFd; + mData = newData; + mSize = mInflatedSize; + mSlotsOffset = newSlotsOffset; + + updateSlotsData(); + } + + LOG(DEBUG) << "Inflated: " << this->toString(); + return OK; + +fail: + LOG(ERROR) << "Failed maybeInflate"; +fail_silent: + ::munmap(newData, mInflatedSize); + ::close(ashmemFd); + return UNKNOWN_ERROR; } -status_t CursorWindow::createFromParcel(Parcel* parcel, CursorWindow** outCursorWindow) { - *outCursorWindow = nullptr; +status_t CursorWindow::createFromParcel(Parcel* parcel, CursorWindow** outWindow) { + *outWindow = nullptr; - String8 name; - status_t result = parcel->readString8(&name); - if (result) return result; + CursorWindow* window = new CursorWindow(); + if (!window) goto fail; + + if (parcel->readString8(&window->mName)) goto fail; + if (parcel->readUint32(&window->mNumRows)) goto fail; + if (parcel->readUint32(&window->mNumColumns)) goto fail; + if (parcel->readUint32(&window->mSize)) goto fail; + + if ((window->mNumRows * window->mNumColumns * kSlotSizeBytes) > window->mSize) { + LOG(ERROR) << "Unexpected size " << window->mSize << " for " << window->mNumRows + << " rows and " << window->mNumColumns << " columns"; + goto fail_silent; + } bool isAshmem; - result = parcel->readBool(&isAshmem); - if (result) return result; - + if (parcel->readBool(&isAshmem)) goto fail; if (isAshmem) { - return createFromParcelAshmem(parcel, name, outCursorWindow); - } else { - return createFromParcelInline(parcel, name, outCursorWindow); - } -} - -status_t CursorWindow::createFromParcelAshmem(Parcel* parcel, String8& name, - CursorWindow** outCursorWindow) { - status_t result; - int actualSize; - int ashmemFd = parcel->readFileDescriptor(); - if (ashmemFd == int(BAD_TYPE)) { - result = BAD_TYPE; - ALOGE("CursorWindow: readFileDescriptor() failed"); - } else { - ssize_t size = ashmem_get_size_region(ashmemFd); - if (size < 0) { - result = UNKNOWN_ERROR; - ALOGE("CursorWindow: ashmem_get_size_region() failed: errno=%d.", errno); - } else { - int dupAshmemFd = ::fcntl(ashmemFd, F_DUPFD_CLOEXEC, 0); - if (dupAshmemFd < 0) { - result = -errno; - ALOGE("CursorWindow: fcntl() failed: errno=%d.", errno); - } else { - // the size of the ashmem descriptor can be modified between ashmem_get_size_region - // call and mmap, so we'll check again immediately after memory is mapped - void* data = ::mmap(NULL, size, PROT_READ, MAP_SHARED, dupAshmemFd, 0); - if (data == MAP_FAILED) { - result = -errno; - ALOGE("CursorWindow: mmap() failed: errno=%d.", errno); - } else if ((actualSize = ashmem_get_size_region(dupAshmemFd)) != size) { - ::munmap(data, size); - result = BAD_VALUE; - ALOGE("CursorWindow: ashmem_get_size_region() returned %d, expected %d" - " errno=%d", - actualSize, (int) size, errno); - } else { - CursorWindow* window = new CursorWindow(name, dupAshmemFd, - data, size, size, true /*readOnly*/); - LOG_WINDOW("Created CursorWindow from ashmem parcel: freeOffset=%d, " - "numRows=%d, numColumns=%d, mSize=%zu, mData=%p", - window->mHeader->freeOffset, - window->mHeader->numRows, - window->mHeader->numColumns, - window->mSize, window->mData); - *outCursorWindow = window; - return OK; - } - ::close(dupAshmemFd); - } + window->mAshmemFd = parcel->readFileDescriptor(); + if (window->mAshmemFd < 0) { + LOG(ERROR) << "Failed readFileDescriptor"; + goto fail_silent; } + + window->mAshmemFd = ::fcntl(window->mAshmemFd, F_DUPFD_CLOEXEC, 0); + if (window->mAshmemFd < 0) { + PLOG(ERROR) << "Failed F_DUPFD_CLOEXEC"; + goto fail_silent; + } + + window->mData = ::mmap(nullptr, window->mSize, PROT_READ, MAP_SHARED, window->mAshmemFd, 0); + if (window->mData == MAP_FAILED) { + PLOG(ERROR) << "Failed mmap"; + goto fail_silent; + } + } else { + window->mAshmemFd = -1; + + if (window->mSize > kInlineSize) { + LOG(ERROR) << "Unexpected size " << window->mSize << " for inline window"; + goto fail_silent; + } + + window->mData = malloc(window->mSize); + if (!window->mData) goto fail; + + if (parcel->read(window->mData, window->mSize)) goto fail; } - *outCursorWindow = NULL; - return result; -} -status_t CursorWindow::createFromParcelInline(Parcel* parcel, String8& name, - CursorWindow** outCursorWindow) { - uint32_t sentSize; - status_t result = parcel->readUint32(&sentSize); - if (result) return result; - if (sentSize > kInlineSize) return NO_MEMORY; + // We just came from a remote source, so we're read-only + // and we can't inflate ourselves + window->mInflatedSize = window->mSize; + window->mReadOnly = true; - void* data = calloc(sentSize, 1); - if (!data) return NO_MEMORY; + window->updateSlotsData(); - result = parcel->read(data, sentSize); - if (result) return result; - - CursorWindow* window = new CursorWindow(name, -1, data, sentSize, - sentSize, true /*readOnly*/); - LOG_WINDOW("Created CursorWindow from inline parcel: freeOffset=%d, " - "numRows=%d, numColumns=%d, mSize=%zu, mData=%p", - window->mHeader->freeOffset, - window->mHeader->numRows, - window->mHeader->numColumns, - window->mSize, window->mData); - *outCursorWindow = window; + LOG(DEBUG) << "Created from parcel: " << window->toString(); + *outWindow = window; return OK; + +fail: + LOG(ERROR) << "Failed createFromParcel"; +fail_silent: + delete window; + return UNKNOWN_ERROR; } status_t CursorWindow::writeToParcel(Parcel* parcel) { - LOG_WINDOW("Writing CursorWindow: freeOffset=%d, " - "numRows=%d, numColumns=%d, mSize=%zu, mData=%p", - mHeader->freeOffset, - mHeader->numRows, - mHeader->numColumns, - mSize, mData); - - status_t result = parcel->writeString8(mName); - if (result) return result; + LOG(DEBUG) << "Writing to parcel: " << this->toString(); + if (parcel->writeString8(mName)) goto fail; + if (parcel->writeUint32(mNumRows)) goto fail; + if (parcel->writeUint32(mNumColumns)) goto fail; if (mAshmemFd != -1) { - result = parcel->writeBool(true); - if (result) return result; - return writeToParcelAshmem(parcel); + if (parcel->writeUint32(mSize)) goto fail; + if (parcel->writeBool(true)) goto fail; + if (parcel->writeDupFileDescriptor(mAshmemFd)) goto fail; } else { - result = parcel->writeBool(false); - if (result) return result; - return writeToParcelInline(parcel); + // Since we know we're going to be read-only on the remote side, + // we can compact ourselves on the wire, with just enough padding + // to ensure our slots stay aligned + size_t slotsSize = mSize - mSlotsOffset; + size_t compactedSize = mAllocOffset + slotsSize; + compactedSize = (compactedSize + 3) & ~3; + if (parcel->writeUint32(compactedSize)) goto fail; + if (parcel->writeBool(false)) goto fail; + void* dest = parcel->writeInplace(compactedSize); + if (!dest) goto fail; + memcpy(static_cast(dest), + static_cast(mData), mAllocOffset); + memcpy(static_cast(dest) + compactedSize - slotsSize, + static_cast(mData) + mSlotsOffset, slotsSize); } -} + return OK; -status_t CursorWindow::writeToParcelAshmem(Parcel* parcel) { - return parcel->writeDupFileDescriptor(mAshmemFd); -} - -status_t CursorWindow::writeToParcelInline(Parcel* parcel) { - status_t result = parcel->writeUint32(mHeader->freeOffset); - if (result) return result; - - return parcel->write(mData, mHeader->freeOffset); +fail: + LOG(ERROR) << "Failed writeToParcel"; +fail_silent: + return UNKNOWN_ERROR; } status_t CursorWindow::clear() { if (mReadOnly) { return INVALID_OPERATION; } - - mHeader->freeOffset = sizeof(Header) + sizeof(RowSlotChunk); - mHeader->firstChunkOffset = sizeof(Header); - mHeader->numRows = 0; - mHeader->numColumns = 0; - - RowSlotChunk* firstChunk = static_cast(offsetToPtr(mHeader->firstChunkOffset)); - firstChunk->nextChunkOffset = 0; + mAllocOffset = 0; + mSlotsOffset = mSize; + mNumRows = 0; + mNumColumns = 0; return OK; } +void CursorWindow::updateSlotsData() { + mSlotsData = static_cast(mData) + mSize - kSlotSizeBytes; +} + +void* CursorWindow::offsetToPtr(uint32_t offset, uint32_t bufferSize = 0) { + if (offset > mSize) { + LOG(ERROR) << "Offset " << offset + << " out of bounds, max value " << mSize; + return nullptr; + } + if (offset + bufferSize > mSize) { + LOG(ERROR) << "End offset " << (offset + bufferSize) + << " out of bounds, max value " << mSize; + return nullptr; + } + return static_cast(mData) + offset; +} + +uint32_t CursorWindow::offsetFromPtr(void* ptr) { + return static_cast(ptr) - static_cast(mData); +} + status_t CursorWindow::setNumColumns(uint32_t numColumns) { if (mReadOnly) { return INVALID_OPERATION; } - - uint32_t cur = mHeader->numColumns; - if ((cur > 0 || mHeader->numRows > 0) && cur != numColumns) { - ALOGE("Trying to go from %d columns to %d", cur, numColumns); + uint32_t cur = mNumColumns; + if ((cur > 0 || mNumRows > 0) && cur != numColumns) { + LOG(ERROR) << "Trying to go from " << cur << " columns to " << numColumns; return INVALID_OPERATION; } - mHeader->numColumns = numColumns; + mNumColumns = numColumns; return OK; } @@ -287,30 +289,18 @@ status_t CursorWindow::allocRow() { if (mReadOnly) { return INVALID_OPERATION; } - - // Fill in the row slot - RowSlot* rowSlot = allocRowSlot(); - if (rowSlot == NULL) { - return NO_MEMORY; + size_t size = mNumColumns * kSlotSizeBytes; + off_t newOffset = mSlotsOffset - size; + if (newOffset < mAllocOffset) { + maybeInflate(); + newOffset = mSlotsOffset - size; + if (newOffset < mAllocOffset) { + return NO_MEMORY; + } } - uint32_t rowSlotOffset = offsetFromPtr(rowSlot); - - // Allocate the slots for the field directory - size_t fieldDirSize = mHeader->numColumns * sizeof(FieldSlot); - uint32_t fieldDirOffset = alloc(fieldDirSize, true /*aligned*/); - if (!fieldDirOffset) { - mHeader->numRows--; - LOG_WINDOW("The row failed, so back out the new row accounting " - "from allocRowSlot %d", mHeader->numRows); - return NO_MEMORY; - } - FieldSlot* fieldDir = static_cast(offsetToPtr(fieldDirOffset)); - memset(fieldDir, 0, fieldDirSize); - - LOG_WINDOW("Allocated row %u, rowSlot is at offset %u, fieldDir is %zu bytes at offset %u\n", - mHeader->numRows - 1, rowSlotOffset, fieldDirSize, fieldDirOffset); - rowSlot = static_cast(offsetToPtr(rowSlotOffset)); - rowSlot->offset = fieldDirOffset; + memset(offsetToPtr(newOffset), 0, size); + mSlotsOffset = newOffset; + mNumRows++; return OK; } @@ -318,90 +308,47 @@ status_t CursorWindow::freeLastRow() { if (mReadOnly) { return INVALID_OPERATION; } - - if (mHeader->numRows > 0) { - mHeader->numRows--; + size_t size = mNumColumns * kSlotSizeBytes; + off_t newOffset = mSlotsOffset + size; + if (newOffset > mSize) { + return NO_MEMORY; } + mSlotsOffset = newOffset; + mNumRows--; return OK; } -uint32_t CursorWindow::alloc(size_t size, bool aligned) { - uint32_t padding; - if (aligned) { - // 4 byte alignment - padding = (~mHeader->freeOffset + 1) & 3; - } else { - padding = 0; +status_t CursorWindow::alloc(size_t size, uint32_t* outOffset) { + if (mReadOnly) { + return INVALID_OPERATION; } - - uint32_t offset = mHeader->freeOffset + padding; - uint32_t nextFreeOffset = offset + size; - if (nextFreeOffset > mSize) { - // Try inflating to ashmem before finally giving up - inflate(); - if (nextFreeOffset > mSize) { - ALOGW("Window is full: requested allocation %zu bytes, " - "free space %zu bytes, window size %zu bytes", - size, freeSpace(), mSize); - return 0; + size_t alignedSize = (size + 3) & ~3; + off_t newOffset = mAllocOffset + alignedSize; + if (newOffset > mSlotsOffset) { + maybeInflate(); + newOffset = mAllocOffset + alignedSize; + if (newOffset > mSlotsOffset) { + return NO_MEMORY; } } - - mHeader->freeOffset = nextFreeOffset; - return offset; -} - -CursorWindow::RowSlot* CursorWindow::getRowSlot(uint32_t row) { - uint32_t chunkPos = row; - RowSlotChunk* chunk = static_cast( - offsetToPtr(mHeader->firstChunkOffset)); - while (chunkPos >= ROW_SLOT_CHUNK_NUM_ROWS) { - chunk = static_cast(offsetToPtr(chunk->nextChunkOffset)); - chunkPos -= ROW_SLOT_CHUNK_NUM_ROWS; - } - return &chunk->slots[chunkPos]; -} - -CursorWindow::RowSlot* CursorWindow::allocRowSlot() { - uint32_t chunkPos = mHeader->numRows; - RowSlotChunk* chunk = static_cast( - offsetToPtr(mHeader->firstChunkOffset)); - while (chunkPos > ROW_SLOT_CHUNK_NUM_ROWS) { - chunk = static_cast(offsetToPtr(chunk->nextChunkOffset)); - chunkPos -= ROW_SLOT_CHUNK_NUM_ROWS; - } - if (chunkPos == ROW_SLOT_CHUNK_NUM_ROWS) { - if (!chunk->nextChunkOffset) { - uint32_t chunkOffset = offsetFromPtr(chunk); - uint32_t newChunk = alloc(sizeof(RowSlotChunk), true /*aligned*/); - chunk = static_cast(offsetToPtr(chunkOffset)); - chunk->nextChunkOffset = newChunk; - if (!chunk->nextChunkOffset) { - return NULL; - } - } - chunk = static_cast(offsetToPtr(chunk->nextChunkOffset)); - chunk->nextChunkOffset = 0; - chunkPos = 0; - } - mHeader->numRows += 1; - return &chunk->slots[chunkPos]; + *outOffset = mAllocOffset; + mAllocOffset = newOffset; + return OK; } CursorWindow::FieldSlot* CursorWindow::getFieldSlot(uint32_t row, uint32_t column) { - if (row >= mHeader->numRows || column >= mHeader->numColumns) { - ALOGE("Failed to read row %d, column %d from a CursorWindow which " - "has %d rows, %d columns.", - row, column, mHeader->numRows, mHeader->numColumns); - return NULL; + if (row >= mNumRows || column >= mNumColumns) { + LOG(ERROR) << "Failed to read row " << row << ", column " << column + << " from a window with " << mNumRows << " rows, " << mNumColumns << " columns"; + return nullptr; } - RowSlot* rowSlot = getRowSlot(row); - if (!rowSlot) { - ALOGE("Failed to find rowSlot for row %d.", row); - return NULL; - } - FieldSlot* fieldDir = static_cast(offsetToPtr(rowSlot->offset)); - return &fieldDir[column]; + + // This is carefully tuned to use as few cycles as + // possible, since this is an extremely hot code path; + // see CursorWindow_bench.cpp for more details + void *result = static_cast(mSlotsData) + - (((row * mNumColumns) + column) << kSlotShift); + return static_cast(result); } status_t CursorWindow::putBlob(uint32_t row, uint32_t column, const void* value, size_t size) { @@ -423,16 +370,15 @@ status_t CursorWindow::putBlobOrString(uint32_t row, uint32_t column, if (!fieldSlot) { return BAD_VALUE; } - uint32_t fieldSlotOffset = offsetFromPtr(fieldSlot); - uint32_t offset = alloc(size); - if (!offset) { + uint32_t offset; + if (alloc(size, &offset)) { return NO_MEMORY; } memcpy(offsetToPtr(offset), value, size); - fieldSlot = static_cast(offsetToPtr(fieldSlotOffset)); + fieldSlot = getFieldSlot(row, column); fieldSlot->type = type; fieldSlot->data.buffer.offset = offset; fieldSlot->data.buffer.size = size; diff --git a/libs/androidfw/include/androidfw/CursorWindow.h b/libs/androidfw/include/androidfw/CursorWindow.h index 0bee60929cc9d..4c4f7335008df 100644 --- a/libs/androidfw/include/androidfw/CursorWindow.h +++ b/libs/androidfw/include/androidfw/CursorWindow.h @@ -20,38 +20,36 @@ #include #include #include +#include -#include -#include -#include +#include "android-base/stringprintf.h" +#include "binder/Parcel.h" +#include "utils/String8.h" -#if LOG_NDEBUG - -#define IF_LOG_WINDOW() if (false) #define LOG_WINDOW(...) -#else - -#define IF_LOG_WINDOW() IF_ALOG(LOG_DEBUG, "CursorWindow") -#define LOG_WINDOW(...) ALOG(LOG_DEBUG, "CursorWindow", __VA_ARGS__) - -#endif - namespace android { /** - * This class stores a set of rows from a database in a buffer. The begining of the - * window has first chunk of RowSlots, which are offsets to the row directory, followed by - * an offset to the next chunk in a linked-list of additional chunk of RowSlots in case - * the pre-allocated chunk isn't big enough to refer to all rows. Each row directory has a - * FieldSlot per column, which has the size, offset, and type of the data for that field. - * Note that the data types come from sqlite3.h. + * This class stores a set of rows from a database in a buffer. Internally + * data is structured as a "heap" of string/blob allocations at the bottom + * of the memory region, and a "stack" of FieldSlot allocations at the top + * of the memory region. Here's an example visual representation: + * + * +----------------------------------------------------------------+ + * |heap\0of\0strings\0 222211110000| ... + * +-------------------+--------------------------------+-------+---+ + * ^ ^ ^ ^ ^ ^ + * | | | | | | + * | +- mAllocOffset mSlotsOffset -+ | | | + * +- mData mSlotsStart -+ | | + * mSize -+ | + * mInflatedSize -+ * * Strings are stored in UTF-8. */ class CursorWindow { - CursorWindow(const String8& name, int ashmemFd, void* data, size_t size, - size_t inflatedSize, bool readOnly); + CursorWindow(); public: /* Field types. */ @@ -88,9 +86,9 @@ public: inline String8 name() { return mName; } inline size_t size() { return mSize; } - inline size_t freeSpace() { return mSize - mHeader->freeOffset; } - inline uint32_t getNumRows() { return mHeader->numRows; } - inline uint32_t getNumColumns() { return mHeader->numColumns; } + inline size_t freeSpace() { return mSlotsOffset - mAllocOffset; } + inline uint32_t getNumRows() { return mNumRows; } + inline uint32_t getNumColumns() { return mNumColumns; } status_t clear(); status_t setNumColumns(uint32_t numColumns); @@ -138,75 +136,56 @@ public: return offsetToPtr(fieldSlot->data.buffer.offset, fieldSlot->data.buffer.size); } + inline std::string toString() const { + return android::base::StringPrintf("CursorWindow{name=%s, fd=%d, size=%d, inflatedSize=%d, " + "allocOffset=%d, slotsOffset=%d, numRows=%d, numColumns=%d}", mName.c_str(), + mAshmemFd, mSize, mInflatedSize, mAllocOffset, mSlotsOffset, mNumRows, mNumColumns); + } + private: - static const size_t ROW_SLOT_CHUNK_NUM_ROWS = 100; - - struct Header { - // Offset of the lowest unused byte in the window. - uint32_t freeOffset; - - // Offset of the first row slot chunk. - uint32_t firstChunkOffset; - - uint32_t numRows; - uint32_t numColumns; - }; - - struct RowSlot { - uint32_t offset; - }; - - struct RowSlotChunk { - RowSlot slots[ROW_SLOT_CHUNK_NUM_ROWS]; - uint32_t nextChunkOffset; - }; - String8 mName; - int mAshmemFd; - void* mData; - size_t mSize; - size_t mInflatedSize; - bool mReadOnly; - Header* mHeader; + int mAshmemFd = -1; + void* mData = nullptr; + /** + * Pointer to the first FieldSlot, used to optimize the extremely + * hot code path of getFieldSlot(). + */ + void* mSlotsData = nullptr; + uint32_t mSize = 0; + /** + * When a window starts as lightweight inline allocation, this value + * holds the "full" size to be created after ashmem inflation. + */ + uint32_t mInflatedSize = 0; + /** + * Offset to the top of the "heap" of string/blob allocations. By + * storing these allocations at the bottom of our memory region we + * avoid having to rewrite offsets when inflating. + */ + uint32_t mAllocOffset = 0; + /** + * Offset to the bottom of the "stack" of FieldSlot allocations. + */ + uint32_t mSlotsOffset = 0; + uint32_t mNumRows = 0; + uint32_t mNumColumns = 0; + bool mReadOnly = false; - inline void* offsetToPtr(uint32_t offset, uint32_t bufferSize = 0) { - if (offset > mSize) { - ALOGE("Offset %" PRIu32 " out of bounds, max value %zu", offset, mSize); - return NULL; - } - if (offset + bufferSize > mSize) { - ALOGE("End offset %" PRIu32 " out of bounds, max value %zu", - offset + bufferSize, mSize); - return NULL; - } - return static_cast(mData) + offset; - } + void updateSlotsData(); - inline uint32_t offsetFromPtr(void* ptr) { - return static_cast(ptr) - static_cast(mData); - } - - static status_t createFromParcelAshmem(Parcel*, String8&, CursorWindow**); - static status_t createFromParcelInline(Parcel*, String8&, CursorWindow**); - - status_t writeToParcelAshmem(Parcel*); - status_t writeToParcelInline(Parcel*); + void* offsetToPtr(uint32_t offset, uint32_t bufferSize); + uint32_t offsetFromPtr(void* ptr); /** * By default windows are lightweight inline allocations; this method * inflates the window into a larger ashmem region. */ - status_t inflate(); + status_t maybeInflate(); /** - * Allocate a portion of the window. Returns the offset - * of the allocation, or 0 if there isn't enough space. - * If aligned is true, the allocation gets 4 byte alignment. + * Allocate a portion of the window. */ - uint32_t alloc(size_t size, bool aligned = false); - - RowSlot* getRowSlot(uint32_t row); - RowSlot* allocRowSlot(); + status_t alloc(size_t size, uint32_t* outOffset); status_t putBlobOrString(uint32_t row, uint32_t column, const void* value, size_t size, int32_t type); diff --git a/libs/androidfw/tests/CursorWindow_bench.cpp b/libs/androidfw/tests/CursorWindow_bench.cpp new file mode 100644 index 0000000000000..f1191c3d72136 --- /dev/null +++ b/libs/androidfw/tests/CursorWindow_bench.cpp @@ -0,0 +1,86 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "benchmark/benchmark.h" + +#include "androidfw/CursorWindow.h" + +namespace android { + +static void BM_CursorWindowWrite(benchmark::State& state, size_t rows, size_t cols) { + CursorWindow* w; + CursorWindow::create(String8("test"), 1 << 21, &w); + + while (state.KeepRunning()) { + w->clear(); + w->setNumColumns(cols); + for (int row = 0; row < rows; row++) { + w->allocRow(); + for (int col = 0; col < cols; col++) { + w->putLong(row, col, 0xcafe); + } + } + } +} + +static void BM_CursorWindowWrite4x4(benchmark::State& state) { + BM_CursorWindowWrite(state, 4, 4); +} +BENCHMARK(BM_CursorWindowWrite4x4); + +static void BM_CursorWindowWrite1Kx4(benchmark::State& state) { + BM_CursorWindowWrite(state, 1024, 4); +} +BENCHMARK(BM_CursorWindowWrite1Kx4); + +static void BM_CursorWindowWrite16Kx4(benchmark::State& state) { + BM_CursorWindowWrite(state, 16384, 4); +} +BENCHMARK(BM_CursorWindowWrite16Kx4); + +static void BM_CursorWindowRead(benchmark::State& state, size_t rows, size_t cols) { + CursorWindow* w; + CursorWindow::create(String8("test"), 1 << 21, &w); + w->setNumColumns(cols); + for (int row = 0; row < rows; row++) { + w->allocRow(); + } + + while (state.KeepRunning()) { + for (int row = 0; row < rows; row++) { + for (int col = 0; col < cols; col++) { + w->getFieldSlot(row, col); + } + } + } +} + +static void BM_CursorWindowRead4x4(benchmark::State& state) { + BM_CursorWindowRead(state, 4, 4); +} +BENCHMARK(BM_CursorWindowRead4x4); + +static void BM_CursorWindowRead1Kx4(benchmark::State& state) { + BM_CursorWindowRead(state, 1024, 4); +} +BENCHMARK(BM_CursorWindowRead1Kx4); + +static void BM_CursorWindowRead16Kx4(benchmark::State& state) { + BM_CursorWindowRead(state, 16384, 4); +} +BENCHMARK(BM_CursorWindowRead16Kx4); + +} // namespace android diff --git a/libs/androidfw/tests/CursorWindow_test.cpp b/libs/androidfw/tests/CursorWindow_test.cpp new file mode 100644 index 0000000000000..dfcf76e6edf65 --- /dev/null +++ b/libs/androidfw/tests/CursorWindow_test.cpp @@ -0,0 +1,359 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include + +#include "androidfw/CursorWindow.h" + +#include "TestHelpers.h" + +#define CREATE_WINDOW_1K \ + CursorWindow* w; \ + CursorWindow::create(String8("test"), 1 << 10, &w); + +#define CREATE_WINDOW_1K_3X3 \ + CursorWindow* w; \ + CursorWindow::create(String8("test"), 1 << 10, &w); \ + ASSERT_EQ(w->setNumColumns(3), OK); \ + ASSERT_EQ(w->allocRow(), OK); \ + ASSERT_EQ(w->allocRow(), OK); \ + ASSERT_EQ(w->allocRow(), OK); + +#define CREATE_WINDOW_2M \ + CursorWindow* w; \ + CursorWindow::create(String8("test"), 1 << 21, &w); + +static constexpr const size_t kHalfInlineSize = 8192; +static constexpr const size_t kGiantSize = 1048576; + +namespace android { + +TEST(CursorWindowTest, Empty) { + CREATE_WINDOW_1K; + + ASSERT_EQ(w->getNumRows(), 0); + ASSERT_EQ(w->getNumColumns(), 0); + ASSERT_EQ(w->size(), 1 << 10); + ASSERT_EQ(w->freeSpace(), 1 << 10); +} + +TEST(CursorWindowTest, SetNumColumns) { + CREATE_WINDOW_1K; + + // Once we've locked in columns, we can't adjust + ASSERT_EQ(w->getNumColumns(), 0); + ASSERT_EQ(w->setNumColumns(4), OK); + ASSERT_NE(w->setNumColumns(5), OK); + ASSERT_NE(w->setNumColumns(3), OK); + ASSERT_EQ(w->getNumColumns(), 4); +} + +TEST(CursorWindowTest, SetNumColumnsAfterRow) { + CREATE_WINDOW_1K; + + // Once we've locked in a row, we can't adjust columns + ASSERT_EQ(w->getNumColumns(), 0); + ASSERT_EQ(w->allocRow(), OK); + ASSERT_NE(w->setNumColumns(4), OK); + ASSERT_EQ(w->getNumColumns(), 0); +} + +TEST(CursorWindowTest, AllocRow) { + CREATE_WINDOW_1K; + + ASSERT_EQ(w->setNumColumns(4), OK); + + // Rolling forward means we have less free space + ASSERT_EQ(w->getNumRows(), 0); + auto before = w->freeSpace(); + ASSERT_EQ(w->allocRow(), OK); + ASSERT_LT(w->freeSpace(), before); + ASSERT_EQ(w->getNumRows(), 1); + + // Verify we can unwind + ASSERT_EQ(w->freeLastRow(), OK); + ASSERT_EQ(w->freeSpace(), before); + ASSERT_EQ(w->getNumRows(), 0); + + // Can't unwind when no rows left + ASSERT_NE(w->freeLastRow(), OK); +} + +TEST(CursorWindowTest, AllocRowBounds) { + CREATE_WINDOW_1K; + + // 60 columns is 960 bytes, which means only a single row can fit + ASSERT_EQ(w->setNumColumns(60), OK); + ASSERT_EQ(w->allocRow(), OK); + ASSERT_NE(w->allocRow(), OK); +} + +TEST(CursorWindowTest, StoreNull) { + CREATE_WINDOW_1K_3X3; + + ASSERT_EQ(w->putNull(1, 1), OK); + ASSERT_EQ(w->putNull(0, 0), OK); + + { + auto field = w->getFieldSlot(1, 1); + ASSERT_EQ(w->getFieldSlotType(field), CursorWindow::FIELD_TYPE_NULL); + } + { + auto field = w->getFieldSlot(0, 0); + ASSERT_EQ(w->getFieldSlotType(field), CursorWindow::FIELD_TYPE_NULL); + } +} + +TEST(CursorWindowTest, StoreLong) { + CREATE_WINDOW_1K_3X3; + + ASSERT_EQ(w->putLong(1, 1, 0xf00d), OK); + ASSERT_EQ(w->putLong(0, 0, 0xcafe), OK); + + { + auto field = w->getFieldSlot(1, 1); + ASSERT_EQ(w->getFieldSlotType(field), CursorWindow::FIELD_TYPE_INTEGER); + ASSERT_EQ(w->getFieldSlotValueLong(field), 0xf00d); + } + { + auto field = w->getFieldSlot(0, 0); + ASSERT_EQ(w->getFieldSlotType(field), CursorWindow::FIELD_TYPE_INTEGER); + ASSERT_EQ(w->getFieldSlotValueLong(field), 0xcafe); + } +} + +TEST(CursorWindowTest, StoreString) { + CREATE_WINDOW_1K_3X3; + + ASSERT_EQ(w->putString(1, 1, "food", 5), OK); + ASSERT_EQ(w->putString(0, 0, "cafe", 5), OK); + + size_t size; + { + auto field = w->getFieldSlot(1, 1); + ASSERT_EQ(w->getFieldSlotType(field), CursorWindow::FIELD_TYPE_STRING); + auto actual = w->getFieldSlotValueString(field, &size); + ASSERT_EQ(std::string(actual), "food"); + } + { + auto field = w->getFieldSlot(0, 0); + ASSERT_EQ(w->getFieldSlotType(field), CursorWindow::FIELD_TYPE_STRING); + auto actual = w->getFieldSlotValueString(field, &size); + ASSERT_EQ(std::string(actual), "cafe"); + } +} + +TEST(CursorWindowTest, StoreBounds) { + CREATE_WINDOW_1K_3X3; + + // Can't work with values beyond bounds + ASSERT_NE(w->putLong(0, 3, 0xcafe), OK); + ASSERT_NE(w->putLong(3, 0, 0xcafe), OK); + ASSERT_NE(w->putLong(3, 3, 0xcafe), OK); + ASSERT_EQ(w->getFieldSlot(0, 3), nullptr); + ASSERT_EQ(w->getFieldSlot(3, 0), nullptr); + ASSERT_EQ(w->getFieldSlot(3, 3), nullptr); +} + +TEST(CursorWindowTest, Inflate) { + CREATE_WINDOW_2M; + + auto before = w->size(); + ASSERT_EQ(w->setNumColumns(4), OK); + ASSERT_EQ(w->allocRow(), OK); + + // Scratch buffer that will fit before inflation + void* buf = malloc(kHalfInlineSize); + + // Store simple value + ASSERT_EQ(w->putLong(0, 0, 0xcafe), OK); + + // Store first object that fits inside + memset(buf, 42, kHalfInlineSize); + ASSERT_EQ(w->putBlob(0, 1, buf, kHalfInlineSize), OK); + ASSERT_EQ(w->size(), before); + + // Store second simple value + ASSERT_EQ(w->putLong(0, 2, 0xface), OK); + + // Store second object that requires inflation + memset(buf, 84, kHalfInlineSize); + ASSERT_EQ(w->putBlob(0, 3, buf, kHalfInlineSize), OK); + ASSERT_GT(w->size(), before); + + // Verify data is intact + { + auto field = w->getFieldSlot(0, 0); + ASSERT_EQ(w->getFieldSlotType(field), CursorWindow::FIELD_TYPE_INTEGER); + ASSERT_EQ(w->getFieldSlotValueLong(field), 0xcafe); + } + { + auto field = w->getFieldSlot(0, 1); + ASSERT_EQ(w->getFieldSlotType(field), CursorWindow::FIELD_TYPE_BLOB); + size_t actualSize; + auto actual = w->getFieldSlotValueBlob(field, &actualSize); + ASSERT_EQ(actualSize, kHalfInlineSize); + memset(buf, 42, kHalfInlineSize); + ASSERT_NE(actual, buf); + ASSERT_EQ(memcmp(buf, actual, kHalfInlineSize), 0); + } + { + auto field = w->getFieldSlot(0, 2); + ASSERT_EQ(w->getFieldSlotType(field), CursorWindow::FIELD_TYPE_INTEGER); + ASSERT_EQ(w->getFieldSlotValueLong(field), 0xface); + } + { + auto field = w->getFieldSlot(0, 3); + ASSERT_EQ(w->getFieldSlotType(field), CursorWindow::FIELD_TYPE_BLOB); + size_t actualSize; + auto actual = w->getFieldSlotValueBlob(field, &actualSize); + ASSERT_EQ(actualSize, kHalfInlineSize); + memset(buf, 84, kHalfInlineSize); + ASSERT_NE(actual, buf); + ASSERT_EQ(memcmp(buf, actual, kHalfInlineSize), 0); + } +} + +TEST(CursorWindowTest, ParcelEmpty) { + CREATE_WINDOW_2M; + + Parcel p; + w->writeToParcel(&p); + p.setDataPosition(0); + w = nullptr; + + ASSERT_EQ(CursorWindow::createFromParcel(&p, &w), OK); + ASSERT_EQ(w->getNumRows(), 0); + ASSERT_EQ(w->getNumColumns(), 0); + ASSERT_EQ(w->size(), 0); + ASSERT_EQ(w->freeSpace(), 0); + + // We can't mutate the window after parceling + ASSERT_NE(w->setNumColumns(4), OK); + ASSERT_NE(w->allocRow(), OK); +} + +TEST(CursorWindowTest, ParcelSmall) { + CREATE_WINDOW_2M; + + auto before = w->size(); + ASSERT_EQ(w->setNumColumns(4), OK); + ASSERT_EQ(w->allocRow(), OK); + + // Scratch buffer that will fit before inflation + void* buf = malloc(kHalfInlineSize); + + // Store simple value + ASSERT_EQ(w->putLong(0, 0, 0xcafe), OK); + + // Store first object that fits inside + memset(buf, 42, kHalfInlineSize); + ASSERT_EQ(w->putBlob(0, 1, buf, kHalfInlineSize), OK); + ASSERT_EQ(w->size(), before); + + // Store second object with zero length + ASSERT_EQ(w->putBlob(0, 2, buf, 0), OK); + ASSERT_EQ(w->size(), before); + + // Force through a parcel + Parcel p; + w->writeToParcel(&p); + p.setDataPosition(0); + w = nullptr; + + ASSERT_EQ(CursorWindow::createFromParcel(&p, &w), OK); + ASSERT_EQ(w->getNumRows(), 1); + ASSERT_EQ(w->getNumColumns(), 4); + + // Verify data is intact + { + auto field = w->getFieldSlot(0, 0); + ASSERT_EQ(w->getFieldSlotType(field), CursorWindow::FIELD_TYPE_INTEGER); + ASSERT_EQ(w->getFieldSlotValueLong(field), 0xcafe); + } + { + auto field = w->getFieldSlot(0, 1); + ASSERT_EQ(w->getFieldSlotType(field), CursorWindow::FIELD_TYPE_BLOB); + size_t actualSize; + auto actual = w->getFieldSlotValueBlob(field, &actualSize); + ASSERT_EQ(actualSize, kHalfInlineSize); + memset(buf, 42, kHalfInlineSize); + ASSERT_NE(actual, buf); + ASSERT_EQ(memcmp(buf, actual, kHalfInlineSize), 0); + } + { + auto field = w->getFieldSlot(0, 2); + ASSERT_EQ(w->getFieldSlotType(field), CursorWindow::FIELD_TYPE_BLOB); + size_t actualSize; + auto actual = w->getFieldSlotValueBlob(field, &actualSize); + ASSERT_EQ(actualSize, 0); + ASSERT_NE(actual, nullptr); + } +} + +TEST(CursorWindowTest, ParcelLarge) { + CREATE_WINDOW_2M; + + ASSERT_EQ(w->setNumColumns(4), OK); + ASSERT_EQ(w->allocRow(), OK); + + // Store simple value + ASSERT_EQ(w->putLong(0, 0, 0xcafe), OK); + + // Store object that forces inflation + void* buf = malloc(kGiantSize); + memset(buf, 42, kGiantSize); + ASSERT_EQ(w->putBlob(0, 1, buf, kGiantSize), OK); + + // Store second object with zero length + ASSERT_EQ(w->putBlob(0, 2, buf, 0), OK); + + // Force through a parcel + Parcel p; + w->writeToParcel(&p); + p.setDataPosition(0); + w = nullptr; + + ASSERT_EQ(CursorWindow::createFromParcel(&p, &w), OK); + ASSERT_EQ(w->getNumRows(), 1); + ASSERT_EQ(w->getNumColumns(), 4); + + // Verify data is intact + { + auto field = w->getFieldSlot(0, 0); + ASSERT_EQ(w->getFieldSlotType(field), CursorWindow::FIELD_TYPE_INTEGER); + ASSERT_EQ(w->getFieldSlotValueLong(field), 0xcafe); + } + { + auto field = w->getFieldSlot(0, 1); + ASSERT_EQ(w->getFieldSlotType(field), CursorWindow::FIELD_TYPE_BLOB); + size_t actualSize; + auto actual = w->getFieldSlotValueBlob(field, &actualSize); + ASSERT_EQ(actualSize, kGiantSize); + memset(buf, 42, kGiantSize); + ASSERT_EQ(memcmp(buf, actual, kGiantSize), 0); + } + { + auto field = w->getFieldSlot(0, 2); + ASSERT_EQ(w->getFieldSlotType(field), CursorWindow::FIELD_TYPE_BLOB); + size_t actualSize; + auto actual = w->getFieldSlotValueBlob(field, &actualSize); + ASSERT_EQ(actualSize, 0); + ASSERT_NE(actual, nullptr); + } +} + +} // android From 256da5a361104276b01ad2834f9929bc8ed457f7 Mon Sep 17 00:00:00 2001 From: Jeff Sharkey Date: Tue, 13 Oct 2020 09:40:52 -0600 Subject: [PATCH 2/2] Add fuzzer for rewritten CursorWindow. We recently rewrote CursorWindow, so let's get a fuzzer wired up to see if it has any bugs. This change creates a separate "libandroidfw_fuzz" library, since we can't link to libbinder when building Windows host-side binaries; the fuzzer doesn't need Window support. And fix our first vulnerability where getFieldSlot() could be tricked into reading out of bounds data. The included corpus seed was generated using this example code: CursorWindow* w = nullptr; CursorWindow::create(android::String8("test"), 1 << 21, &w); w->setNumColumns(3); w->allocRow(); w->putLong(0,0,0xcafe); w->putLong(0,1,0xcafe); w->putLong(0,2,0xcafe); // Row purposefully left empty w->allocRow(); w->allocRow(); w->putNull(2,0); w->putNull(2,1); w->putNull(2,2); w->allocRow(); w->putString(3,0,"cafe",5); w->putString(3,1,"cafe",5); w->putString(3,2,"cafe",5); w->allocRow(); w->putDouble(4,0,3.14159f); w->putDouble(4,1,3.14159f); w->putDouble(4,2,3.14159f); Parcel p; w->writeToParcel(&p); Bug: 169251528 Test: atest libandroidfw_tests:CursorWindowTest Test: SANITIZE_HOST=address make ${FUZZER_NAME} && ${ANDROID_HOST_OUT}/fuzz/$(get_build_var HOST_ARCH)/${FUZZER_NAME}/${FUZZER_NAME} Change-Id: I405d377900943de0ad732d3f1a1a0970e17d5140 --- libs/androidfw/Android.bp | 18 ++++ libs/androidfw/CursorWindow.cpp | 21 +++-- .../fuzz/cursorwindow_fuzzer/Android.bp | 31 +++++++ .../cursorwindow_fuzzer/corpus/typical.bin | Bin 0 -> 292 bytes .../cursorwindow_fuzzer.cpp | 78 ++++++++++++++++++ .../include/androidfw/CursorWindow.h | 3 +- 6 files changed, 141 insertions(+), 10 deletions(-) create mode 100644 libs/androidfw/fuzz/cursorwindow_fuzzer/Android.bp create mode 100644 libs/androidfw/fuzz/cursorwindow_fuzzer/corpus/typical.bin create mode 100644 libs/androidfw/fuzz/cursorwindow_fuzzer/cursorwindow_fuzzer.cpp diff --git a/libs/androidfw/Android.bp b/libs/androidfw/Android.bp index 45ad94fdb75ac..903ca2aa07836 100644 --- a/libs/androidfw/Android.bp +++ b/libs/androidfw/Android.bp @@ -193,3 +193,21 @@ cc_benchmark { shared_libs: common_test_libs, data: ["tests/data/**/*.apk"], } + +cc_library { + name: "libandroidfw_fuzzer_lib", + defaults: ["libandroidfw_defaults"], + host_supported: true, + srcs: [ + "CursorWindow.cpp", + ], + export_include_dirs: ["include"], + target: { + android: { + shared_libs: common_test_libs + ["libbinder", "liblog"], + }, + host: { + static_libs: common_test_libs + ["libbinder", "liblog"], + }, + }, +} diff --git a/libs/androidfw/CursorWindow.cpp b/libs/androidfw/CursorWindow.cpp index 1e6de67a40ba8..915c0d75a2803 100644 --- a/libs/androidfw/CursorWindow.cpp +++ b/libs/androidfw/CursorWindow.cpp @@ -251,7 +251,8 @@ status_t CursorWindow::clear() { } void CursorWindow::updateSlotsData() { - mSlotsData = static_cast(mData) + mSize - kSlotSizeBytes; + mSlotsStart = static_cast(mData) + mSize - kSlotSizeBytes; + mSlotsEnd = static_cast(mData) + mSlotsOffset; } void* CursorWindow::offsetToPtr(uint32_t offset, uint32_t bufferSize = 0) { @@ -300,6 +301,7 @@ status_t CursorWindow::allocRow() { } memset(offsetToPtr(newOffset), 0, size); mSlotsOffset = newOffset; + updateSlotsData(); mNumRows++; return OK; } @@ -314,6 +316,7 @@ status_t CursorWindow::freeLastRow() { return NO_MEMORY; } mSlotsOffset = newOffset; + updateSlotsData(); mNumRows--; return OK; } @@ -337,18 +340,18 @@ status_t CursorWindow::alloc(size_t size, uint32_t* outOffset) { } CursorWindow::FieldSlot* CursorWindow::getFieldSlot(uint32_t row, uint32_t column) { - if (row >= mNumRows || column >= mNumColumns) { - LOG(ERROR) << "Failed to read row " << row << ", column " << column - << " from a window with " << mNumRows << " rows, " << mNumColumns << " columns"; - return nullptr; - } - // This is carefully tuned to use as few cycles as // possible, since this is an extremely hot code path; // see CursorWindow_bench.cpp for more details - void *result = static_cast(mSlotsData) + void *result = static_cast(mSlotsStart) - (((row * mNumColumns) + column) << kSlotShift); - return static_cast(result); + if (result < mSlotsEnd || column >= mNumColumns) { + LOG(ERROR) << "Failed to read row " << row << ", column " << column + << " from a window with " << mNumRows << " rows, " << mNumColumns << " columns"; + return nullptr; + } else { + return static_cast(result); + } } status_t CursorWindow::putBlob(uint32_t row, uint32_t column, const void* value, size_t size) { diff --git a/libs/androidfw/fuzz/cursorwindow_fuzzer/Android.bp b/libs/androidfw/fuzz/cursorwindow_fuzzer/Android.bp new file mode 100644 index 0000000000000..2dac47b0dac6e --- /dev/null +++ b/libs/androidfw/fuzz/cursorwindow_fuzzer/Android.bp @@ -0,0 +1,31 @@ +cc_fuzz { + name: "cursorwindow_fuzzer", + srcs: [ + "cursorwindow_fuzzer.cpp", + ], + host_supported: true, + corpus: ["corpus/*"], + static_libs: ["libgmock"], + target: { + android: { + shared_libs: [ + "libandroidfw_fuzzer_lib", + "libbase", + "libbinder", + "libcutils", + "liblog", + "libutils", + ], + }, + host: { + static_libs: [ + "libandroidfw_fuzzer_lib", + "libbase", + "libbinder", + "libcutils", + "liblog", + "libutils", + ], + }, + }, +} diff --git a/libs/androidfw/fuzz/cursorwindow_fuzzer/corpus/typical.bin b/libs/androidfw/fuzz/cursorwindow_fuzzer/corpus/typical.bin new file mode 100644 index 0000000000000000000000000000000000000000..c7e22dd26ea77c364a69dd3c9c231a63e9a9da8d GIT binary patch literal 292 zcmZQ!U|=XoEiM6446H!R48$CaARYrla$;I4!@hm{&}k-!G{Y}NP6rT^5Fccw01$)B c2LZ4hKpL435{L0YY+3@4_x_zi*g%L60Csp5?f?J) literal 0 HcmV?d00001 diff --git a/libs/androidfw/fuzz/cursorwindow_fuzzer/cursorwindow_fuzzer.cpp b/libs/androidfw/fuzz/cursorwindow_fuzzer/cursorwindow_fuzzer.cpp new file mode 100644 index 0000000000000..8dce212201999 --- /dev/null +++ b/libs/androidfw/fuzz/cursorwindow_fuzzer/cursorwindow_fuzzer.cpp @@ -0,0 +1,78 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include +#include +#include +#include + +#include "android-base/logging.h" +#include "androidfw/CursorWindow.h" +#include "binder/Parcel.h" + +#include + +using android::CursorWindow; +using android::Parcel; + +extern "C" int LLVMFuzzerInitialize(int *, char ***) { + setenv("ANDROID_LOG_TAGS", "*:s", 1); + android::base::InitLogging(nullptr, &android::base::StderrLogger); + return 0; +} + +extern "C" int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) { + Parcel p; + p.setData(data, size); + + CursorWindow* w = nullptr; + if (!CursorWindow::createFromParcel(&p, &w)) { + LOG(WARNING) << "Valid cursor with " << w->getNumRows() << " rows, " + << w->getNumColumns() << " cols"; + + // Try obtaining heap allocations for most items; we trim the + // search space to speed things up + auto rows = std::min(w->getNumRows(), static_cast(128)); + auto cols = std::min(w->getNumColumns(), static_cast(128)); + for (auto row = 0; row < rows; row++) { + for (auto col = 0; col < cols; col++) { + auto field = w->getFieldSlot(row, col); + if (!field) continue; + switch (w->getFieldSlotType(field)) { + case CursorWindow::FIELD_TYPE_STRING: { + size_t size; + w->getFieldSlotValueString(field, &size); + break; + } + case CursorWindow::FIELD_TYPE_BLOB: { + size_t size; + w->getFieldSlotValueBlob(field, &size); + break; + } + } + } + } + + // Finally, try obtaining the furthest valid field + if (rows > 0 && cols > 0) { + w->getFieldSlot(w->getNumRows() - 1, w->getNumColumns() - 1); + } + } + delete w; + + return 0; +} diff --git a/libs/androidfw/include/androidfw/CursorWindow.h b/libs/androidfw/include/androidfw/CursorWindow.h index 4c4f7335008df..6e55a9a0eb8b9 100644 --- a/libs/androidfw/include/androidfw/CursorWindow.h +++ b/libs/androidfw/include/androidfw/CursorWindow.h @@ -150,7 +150,8 @@ private: * Pointer to the first FieldSlot, used to optimize the extremely * hot code path of getFieldSlot(). */ - void* mSlotsData = nullptr; + void* mSlotsStart = nullptr; + void* mSlotsEnd = nullptr; uint32_t mSize = 0; /** * When a window starts as lightweight inline allocation, this value