Vulkan: Add more data to blob cache header
This is to potentially increase blob cache robustness against
collisions and partially modified data. This CL adds 7 bytes
per chunk (8 bytes in total). During decompression, the data
will be checked in order to determine if the cache data can
be used.
* Added chunk index to the header. (1 byte)
* Added CRC16 of the compressed data to the header. (2 bytes)
* Added the original (uncompressed) data size to the header. (4 bytes)
* Moved kBlobHeaderSize to the anonymous namespace.
* Added the following GN flag to enable CRC calculation for the cache.
* angle_enable_crc_for_pipeline_cache (only available in debug mode)
* Corresponds to kEnableCRCForPipelineCache.
* When it is false, the CRC is set to 0.
Bug: b/246683126
Change-Id: I9aaf9cda52e0af07a2e1c2d0c39aca407e515701
Reviewed-on: https://chromium-review.googlesource.com/c/angle/angle/+/4378717
Commit-Queue: Amirali Abdolrashidi <abdolrashidi@google.com>
Reviewed-by: Charlie Lao <cclao@google.com>
Reviewed-by: Shahbaz Youssefi <syoussefi@chromium.org>
diff --git a/src/libANGLE/renderer/vulkan/BUILD.gn b/src/libANGLE/renderer/vulkan/BUILD.gn
index f6f571a..06092f4 100644
--- a/src/libANGLE/renderer/vulkan/BUILD.gn
+++ b/src/libANGLE/renderer/vulkan/BUILD.gn
@@ -31,6 +31,9 @@
# Enable shared ring buffer command buffer allocator
angle_enable_vulkan_shared_ring_buffer_cmd_alloc = false
+
+ # Enable using CRC for pipeline cache data (for debug mode)
+ angle_enable_crc_for_pipeline_cache = false
}
angle_source_set("angle_vk_mem_alloc_wrapper") {
@@ -75,6 +78,12 @@
if (angle_enable_memory_alloc_logging) {
defines += [ "ANGLE_ENABLE_MEMORY_ALLOC_LOGGING=1" ]
}
+
+ # Ensures that CRC is used only in debug mode.
+ assert(is_debug || !angle_enable_crc_for_pipeline_cache)
+ if (angle_enable_crc_for_pipeline_cache) {
+ defines += [ "ANGLE_ENABLE_CRC_FOR_PIPELINE_CACHE" ]
+ }
}
template("angle_vulkan_backend_template") {
diff --git a/src/libANGLE/renderer/vulkan/RendererVk.cpp b/src/libANGLE/renderer/vulkan/RendererVk.cpp
index 42c3bd3..5c32922 100644
--- a/src/libANGLE/renderer/vulkan/RendererVk.cpp
+++ b/src/libANGLE/renderer/vulkan/RendererVk.cpp
@@ -46,6 +46,12 @@
#else
constexpr bool kExposeNonConformantExtensionsAndVersions = false;
#endif
+
+#if defined(ANGLE_ENABLE_CRC_FOR_PIPELINE_CACHE)
+constexpr bool kEnableCRCForPipelineCache = true;
+#else
+constexpr bool kEnableCRCForPipelineCache = false;
+#endif
} // anonymous namespace
namespace rx
@@ -73,6 +79,13 @@
// initialization logic simpler.
constexpr uint32_t kPreferredVulkanAPIVersion = VK_API_VERSION_1_1;
+// For pipeline cache, the values stored in key data has the following format: {originalCacheSize,
+// compressedDataCRC, numChunks, chunkIndex; chunkCompressedData}. The header values are used to
+// validate the data. For example, if the original and compressed sizes are 70000 bytes (68k) and
+// 68841 bytes (67k), the compressed data will be divided into two chunks: {70000,crc0,2,0;34421
+// bytes} and {70000,crc1,2,1;34420 bytes}.
+constexpr size_t kBlobHeaderSize = 8 * sizeof(uint8_t);
+
bool IsVulkan11(uint32_t apiVersion)
{
return apiVersion >= VK_API_VERSION_1_1;
@@ -838,6 +851,54 @@
return true;
}
+// CRC16-CCITT is used for header before the pipeline cache key data.
+uint16_t ComputeCRC16(const uint8_t *data, const size_t size)
+{
+ constexpr uint16_t kPolynomialCRC16 = 0x8408;
+ uint16_t rem = 0;
+
+ for (size_t i = 0; i < size; i++)
+ {
+ rem ^= data[i];
+ for (int j = 0; j < 8; j++)
+ {
+ rem = (rem & 1) ? kPolynomialCRC16 ^ (rem >> 1) : rem >> 1;
+ }
+ }
+ return rem;
+}
+
+// Pack header data for the pipeline cache key data.
+void PackHeaderDataForPipelineCache(uint32_t cacheDataSize,
+ uint16_t compressedDataCRC,
+ uint8_t numChunks,
+ uint8_t chunkIndex,
+ uint64_t *dataOut)
+{
+ uint64_t concatenatedData = cacheDataSize;
+ concatenatedData = (concatenatedData << 16) | compressedDataCRC;
+ concatenatedData = (concatenatedData << 8) | numChunks;
+ concatenatedData = (concatenatedData << 8) | chunkIndex;
+
+ *dataOut = concatenatedData;
+}
+
+// Unpack header data from the pipeline cache key data.
+void UnpackHeaderDataForPipelineCache(uint64_t data,
+ uint32_t *cacheDataSizeOut,
+ uint16_t *compressedDataCRCOut,
+ size_t *numChunksOut,
+ size_t *chunkIndexOut)
+{
+ *chunkIndexOut = data & 0xFF;
+ data >>= 8;
+ *numChunksOut = data & 0xFF;
+ data >>= 8;
+ *compressedDataCRCOut = data & 0xFFFF;
+ data >>= 16;
+ *cacheDataSizeOut = static_cast<uint32_t>(data);
+}
+
void ComputePipelineCacheVkChunkKey(VkPhysicalDeviceProperties physicalDeviceProperties,
const uint8_t chunkIndex,
egl::BlobCache::Key *hashOut)
@@ -894,17 +955,17 @@
// pipelineCache into several parts to store seperately. There is no function to
// query the limit size in android.
constexpr size_t kMaxBlobCacheSize = 64 * 1024;
-
- // Store {numChunks, chunkCompressedData} in keyData, numChunks is used to validate the data.
- // For example, if the compressed size is 68841 bytes(67k), divide into {2,34421 bytes} and
- // {2,34420 bytes}.
- constexpr size_t kBlobHeaderSize = sizeof(uint8_t);
- size_t compressedOffset = 0;
+ size_t compressedOffset = 0;
const size_t numChunks = UnsignedCeilDivide(static_cast<unsigned int>(compressedData.size()),
kMaxBlobCacheSize - kBlobHeaderSize);
size_t chunkSize = UnsignedCeilDivide(static_cast<unsigned int>(compressedData.size()),
static_cast<unsigned int>(numChunks));
+ uint16_t compressedDataCRC = 0;
+ if (kEnableCRCForPipelineCache)
+ {
+ compressedDataCRC = ComputeCRC16(compressedData.data(), compressedData.size());
+ }
for (size_t chunkIndex = 0; chunkIndex < numChunks; ++chunkIndex)
{
@@ -921,8 +982,14 @@
return;
}
- ASSERT(numChunks <= UINT8_MAX);
- keyData.data()[0] = static_cast<uint8_t>(numChunks);
+ // Add the header data, followed by the compressed data.
+ ASSERT(numChunks <= UINT8_MAX && chunkIndex <= UINT8_MAX && cacheData.size() <= UINT32_MAX);
+ uint64_t headerData;
+ PackHeaderDataForPipelineCache(static_cast<uint32_t>(cacheData.size()), compressedDataCRC,
+ static_cast<uint8_t>(numChunks),
+ static_cast<uint8_t>(chunkIndex), &headerData);
+ *reinterpret_cast<uint64_t *>(keyData.data()) = headerData;
+
memcpy(keyData.data() + kBlobHeaderSize, compressedData.data() + compressedOffset,
chunkSize);
compressedOffset += chunkSize;
@@ -979,12 +1046,14 @@
angle::MemoryBuffer *uncompressedData,
bool *success)
{
+ // Make sure that the bool output is initialized to false.
+ *success = false;
+
// Compute the hash key of chunkIndex 0 and find the first cache data in blob cache.
egl::BlobCache::Key chunkCacheHash;
ComputePipelineCacheVkChunkKey(physicalDeviceProperties, 0, &chunkCacheHash);
egl::BlobCache::Value keyData;
- size_t keySize = 0;
- constexpr size_t kBlobHeaderSize = sizeof(uint8_t);
+ size_t keySize = 0;
if (!displayVk->getBlobCache()->get(displayVk->getScratchBuffer(), chunkCacheHash, &keyData,
&keySize) ||
@@ -994,8 +1063,18 @@
return angle::Result::Continue;
}
- // Get the number of chunks.
- size_t numChunks = keyData.data()[0];
+ // Get the number of chunks and other values from the header for data validation.
+ uint32_t uncompressedCacheDataSize;
+ uint16_t compressedDataCRC;
+ size_t numChunks;
+ size_t chunkIndex0;
+
+ uint64_t headerData = *reinterpret_cast<const uint64_t *>(keyData.data());
+ UnpackHeaderDataForPipelineCache(headerData, &uncompressedCacheDataSize, &compressedDataCRC,
+ &numChunks, &chunkIndex0);
+ ASSERT(chunkIndex0 == 0);
+ ASSERT(kEnableCRCForPipelineCache || compressedDataCRC == 0);
+
size_t chunkSize = keySize - kBlobHeaderSize;
size_t compressedSize = 0;
@@ -1019,18 +1098,38 @@
return angle::Result::Continue;
}
- size_t checkNumber = keyData.data()[0];
- chunkSize = keySize - kBlobHeaderSize;
+ // Validate the header values and ensure there is enough space to store.
+ uint32_t checkUncompressedCacheDataSize;
+ uint16_t checkCompressedDataCRC;
+ size_t checkNumChunks;
+ size_t checkChunkIndex;
- if (checkNumber != numChunks || compressedData.size() < (compressedSize + chunkSize))
+ headerData = *reinterpret_cast<const uint64_t *>(keyData.data());
+ UnpackHeaderDataForPipelineCache(headerData, &checkUncompressedCacheDataSize,
+ &checkCompressedDataCRC, &checkNumChunks,
+ &checkChunkIndex);
+ ASSERT(kEnableCRCForPipelineCache || checkCompressedDataCRC == 0);
+
+ chunkSize = keySize - kBlobHeaderSize;
+ bool isHeaderDataCorrupted =
+ (checkNumChunks != numChunks) ||
+ (checkUncompressedCacheDataSize != uncompressedCacheDataSize) ||
+ (checkCompressedDataCRC != compressedDataCRC) || (checkChunkIndex != chunkIndex) ||
+ (compressedData.size() < compressedSize + chunkSize);
+ if (isHeaderDataCorrupted)
{
- // Validate the number value and enough space to store.
- WARN() << "Pipeline cache chunk header corrupted: checkNumber = " << checkNumber
- << ", numChunks = " << numChunks
+ WARN() << "Pipeline cache chunk header corrupted: "
+ << "checkNumChunks = " << checkNumChunks << ", numChunks = " << numChunks
+ << ", checkUncompressedCacheDataSize = " << checkUncompressedCacheDataSize
+ << ", uncompressedCacheDataSize = " << uncompressedCacheDataSize
+ << ", checkCompressedDataCRC = " << checkCompressedDataCRC
+ << ", compressedDataCRC = " << compressedDataCRC
+ << ", checkChunkIndex = " << checkChunkIndex << ", chunkIndex = " << chunkIndex
<< ", compressedData.size() = " << compressedData.size()
<< ", (compressedSize + chunkSize) = " << (compressedSize + chunkSize);
return angle::Result::Continue;
}
+
memcpy(compressedData.data() + compressedSize, keyData.data() + kBlobHeaderSize, chunkSize);
compressedSize += chunkSize;
}
@@ -1040,6 +1139,25 @@
egl::DecompressBlobCacheData(compressedData.data(), compressedSize, uncompressedData),
VK_ERROR_INITIALIZATION_FAILED);
+ // CRC for compressed data and size for decompressed data should match the values in the header.
+ if (kEnableCRCForPipelineCache)
+ {
+ uint16_t computedCompressedDataCRC = ComputeCRC16(compressedData.data(), compressedSize);
+ if (computedCompressedDataCRC != compressedDataCRC)
+ {
+ FATAL() << "Expected CRC = " << compressedDataCRC
+ << ", Actual CRC = " << computedCompressedDataCRC;
+ return angle::Result::Stop;
+ }
+ }
+
+ if (uncompressedData->size() != uncompressedCacheDataSize)
+ {
+ WARN() << "Expected uncompressed size = " << uncompressedCacheDataSize
+ << ", Actual uncompressed size = " << uncompressedData->size();
+ return angle::Result::Continue;
+ }
+
*success = true;
return angle::Result::Continue;
}