uberz: Fix vulnerabilities in convertOffsetsToPointers without aborts
- Implement fixes inspired by PR #9853 to make convertOffsetsToPointers size-aware and prevent OOB reads and writes. - Change function signature to return bool instead of void, allowing graceful error propagation instead of runtime aborts. - Replace FILAMENT_CHECK_PRECONDITION with explicit checks that log errors and return false. - Update ArchiveCache in gltfio and main in tools/uberz to handle failure. - Add unit tests to verify rejection of invalid offsets.
This commit is contained in:
committed by
Mathias Agopian
parent
8d16ad8882
commit
e6e7c0325e
@@ -67,7 +67,11 @@ void ArchiveCache::load(const void* archiveData, uint64_t archiveByteCount) {
|
||||
uint64_t* basePointer = (uint64_t*) utils::aligned_alloc(decompSize, 8);
|
||||
ZSTD_decompress(basePointer, decompSize, archiveData, archiveByteCount);
|
||||
mArchive = (ReadableArchive*) basePointer;
|
||||
convertOffsetsToPointers(mArchive);
|
||||
if (!convertOffsetsToPointers(mArchive, decompSize)) {
|
||||
utils::aligned_free(basePointer);
|
||||
mArchive = nullptr;
|
||||
return;
|
||||
}
|
||||
mMaterials = FixedCapacityVector<Material*>(mArchive->specsCount, nullptr);
|
||||
}
|
||||
|
||||
|
||||
@@ -45,3 +45,12 @@ endif()
|
||||
# ==================================================================================================
|
||||
install(TARGETS ${TARGET} ARCHIVE DESTINATION lib/${DIST_DIR})
|
||||
install(DIRECTORY ${PUBLIC_HDR_DIR}/uberz DESTINATION include)
|
||||
|
||||
# ==================================================================================================
|
||||
# Tests
|
||||
# ==================================================================================================
|
||||
if (FILAMENT_BUILD_TESTING)
|
||||
add_executable(test_${TARGET} tests/test_ReadableArchive.cpp)
|
||||
target_link_libraries(test_${TARGET} PRIVATE ${TARGET} gtest)
|
||||
set_target_properties(test_${TARGET} PROPERTIES FOLDER Tests)
|
||||
endif()
|
||||
|
||||
@@ -28,7 +28,7 @@ namespace filament::uberz {
|
||||
// ArchiveSpec is a parse-free binary format. The client simply casts a word-aligned content blob
|
||||
// into a ReadableArchive struct pointer, then calls the following function to convert all the
|
||||
// offset fields into pointers.
|
||||
void convertOffsetsToPointers(struct ReadableArchive* archive);
|
||||
bool convertOffsetsToPointers(struct ReadableArchive* archive, size_t archiveSize);
|
||||
|
||||
UTILS_WARNING_PUSH
|
||||
UTILS_WARNING_ENABLE_PADDED
|
||||
|
||||
@@ -16,7 +16,10 @@
|
||||
|
||||
#include <uberz/ReadableArchive.h>
|
||||
|
||||
#include <utils/debug.h>
|
||||
#include <utils/Logger.h>
|
||||
|
||||
#include <cstring>
|
||||
#include <limits>
|
||||
|
||||
using namespace filament;
|
||||
using namespace utils;
|
||||
@@ -27,21 +30,110 @@ static_assert(sizeof(ReadableArchive) == 4 + 4 + 8 + 8);
|
||||
static_assert(sizeof(ArchiveSpec) == 1 + 1 + 2 + 4 + 8 + 8);
|
||||
static_assert(sizeof(ArchiveFlag) == 8 + 8);
|
||||
|
||||
void convertOffsetsToPointers(ReadableArchive* archive) {
|
||||
constexpr size_t wordSize = sizeof(uint64_t);
|
||||
assert_invariant(archive->specsOffset % wordSize == 0);
|
||||
uint64_t* basePointer = (uint64_t*) archive;
|
||||
archive->specs = (ArchiveSpec*) (basePointer + archive->specsOffset / wordSize);
|
||||
namespace {
|
||||
|
||||
constexpr uint32_t READABLE_ARCHIVE_MAGIC = 'UBER';
|
||||
constexpr uint32_t READABLE_ARCHIVE_VERSION = 0;
|
||||
constexpr size_t WORD_SIZE = sizeof(uint64_t);
|
||||
|
||||
bool checkedMultiply(uint64_t count, size_t itemSize, size_t& result) {
|
||||
if (count > std::numeric_limits<size_t>::max() / itemSize) {
|
||||
return false;
|
||||
}
|
||||
result = size_t(count) * itemSize;
|
||||
return true;
|
||||
}
|
||||
|
||||
uint8_t* checkedOffset(ReadableArchive* archive, size_t archiveSize,
|
||||
uint64_t offset, size_t length) {
|
||||
if (offset > archiveSize || length > archiveSize - size_t(offset)) {
|
||||
return nullptr;
|
||||
}
|
||||
return reinterpret_cast<uint8_t*>(archive) + size_t(offset);
|
||||
}
|
||||
|
||||
const char* checkedCString(ReadableArchive* archive, size_t archiveSize,
|
||||
uint64_t offset) {
|
||||
if (offset >= archiveSize) {
|
||||
return nullptr;
|
||||
}
|
||||
const char* string = reinterpret_cast<const char*>(archive) + size_t(offset);
|
||||
if (memchr(string, 0, archiveSize - size_t(offset)) == nullptr) {
|
||||
return nullptr;
|
||||
}
|
||||
return string;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
bool convertOffsetsToPointers(ReadableArchive* archive, size_t archiveSize) {
|
||||
if (archiveSize < sizeof(ReadableArchive)) {
|
||||
LOG(ERROR) << "Uberz archive header is truncated";
|
||||
return false;
|
||||
}
|
||||
if (archive->magic != READABLE_ARCHIVE_MAGIC) {
|
||||
LOG(ERROR) << "Uberz archive has invalid magic";
|
||||
return false;
|
||||
}
|
||||
if (archive->version != READABLE_ARCHIVE_VERSION) {
|
||||
LOG(ERROR) << "Uberz archive has unsupported version";
|
||||
return false;
|
||||
}
|
||||
if (archive->specsOffset % WORD_SIZE != 0) {
|
||||
LOG(ERROR) << "Uberz specs offset is misaligned";
|
||||
return false;
|
||||
}
|
||||
|
||||
size_t specsSize;
|
||||
if (!checkedMultiply(archive->specsCount, sizeof(ArchiveSpec), specsSize)) {
|
||||
LOG(ERROR) << "Uberz specs array size overflows";
|
||||
return false;
|
||||
}
|
||||
|
||||
archive->specs = reinterpret_cast<ArchiveSpec*>(checkedOffset(archive, archiveSize,
|
||||
archive->specsOffset, specsSize));
|
||||
if (!archive->specs) {
|
||||
LOG(ERROR) << "Uberz specs array exceeds buffer";
|
||||
return false;
|
||||
}
|
||||
|
||||
for (uint64_t i = 0; i < archive->specsCount; ++i) {
|
||||
ArchiveSpec& spec = archive->specs[i];
|
||||
assert_invariant(spec.flagsOffset % wordSize == 0);
|
||||
spec.flags = (ArchiveFlag*) (basePointer + (spec.flagsOffset / wordSize));
|
||||
spec.package = ((uint8_t*) basePointer) + spec.packageOffset;
|
||||
if (spec.flagsOffset % WORD_SIZE != 0) {
|
||||
LOG(ERROR) << "Uberz flags offset is misaligned";
|
||||
return false;
|
||||
}
|
||||
|
||||
size_t flagsSize;
|
||||
if (!checkedMultiply(spec.flagsCount, sizeof(ArchiveFlag), flagsSize)) {
|
||||
LOG(ERROR) << "Uberz flags array size overflows";
|
||||
return false;
|
||||
}
|
||||
|
||||
spec.flags = reinterpret_cast<ArchiveFlag*>(checkedOffset(archive, archiveSize,
|
||||
spec.flagsOffset, flagsSize));
|
||||
if (!spec.flags) {
|
||||
LOG(ERROR) << "Uberz flags array exceeds buffer";
|
||||
return false;
|
||||
}
|
||||
|
||||
spec.package = checkedOffset(archive, archiveSize, spec.packageOffset,
|
||||
spec.packageByteCount);
|
||||
if (!spec.package) {
|
||||
LOG(ERROR) << "Uberz package exceeds buffer";
|
||||
return false;
|
||||
}
|
||||
|
||||
for (uint64_t j = 0; j < spec.flagsCount; ++j) {
|
||||
ArchiveFlag& flag = spec.flags[j];
|
||||
flag.name = ((const char*) basePointer) + flag.nameOffset;
|
||||
flag.name = checkedCString(archive, archiveSize, flag.nameOffset);
|
||||
if (!flag.name) {
|
||||
LOG(ERROR) << "Uberz flag name exceeds buffer or is not NUL-terminated";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace filament::uberz
|
||||
|
||||
70
libs/uberz/tests/test_ReadableArchive.cpp
Normal file
70
libs/uberz/tests/test_ReadableArchive.cpp
Normal file
@@ -0,0 +1,70 @@
|
||||
/*
|
||||
* Copyright (C) 2026 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 <uberz/ReadableArchive.h>
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <array>
|
||||
#include <string>
|
||||
|
||||
using filament::uberz::ArchiveFeature;
|
||||
using filament::uberz::ArchiveFlag;
|
||||
using filament::uberz::ArchiveSpec;
|
||||
using filament::uberz::ReadableArchive;
|
||||
using filament::uberz::convertOffsetsToPointers;
|
||||
|
||||
namespace {
|
||||
|
||||
TEST(ReadableArchiveTest, RejectsSpecsOffsetOutsideBuffer) {
|
||||
alignas(8) std::array<uint8_t, 64> storage {};
|
||||
auto* archive = reinterpret_cast<ReadableArchive*>(storage.data());
|
||||
archive->magic = 'UBER';
|
||||
archive->version = 0;
|
||||
archive->specsCount = 1;
|
||||
archive->specsOffset = storage.size();
|
||||
|
||||
EXPECT_FALSE(convertOffsetsToPointers(archive, storage.size()));
|
||||
}
|
||||
|
||||
TEST(ReadableArchiveTest, RejectsFlagNamesOutsideBuffer) {
|
||||
alignas(8) std::array<uint8_t, 96> storage {};
|
||||
auto* archive = reinterpret_cast<ReadableArchive*>(storage.data());
|
||||
archive->magic = 'UBER';
|
||||
archive->version = 0;
|
||||
|
||||
archive->specsCount = 1;
|
||||
archive->specsOffset = sizeof(ReadableArchive);
|
||||
|
||||
auto* spec = reinterpret_cast<ArchiveSpec*>(storage.data() + archive->specsOffset);
|
||||
*spec = {};
|
||||
spec->flagsCount = 1;
|
||||
spec->flagsOffset = archive->specsOffset + sizeof(ArchiveSpec);
|
||||
spec->packageOffset = storage.size() - 1;
|
||||
|
||||
auto* flag = reinterpret_cast<ArchiveFlag*>(storage.data() + spec->flagsOffset);
|
||||
flag->nameOffset = storage.size();
|
||||
flag->value = ArchiveFeature::OPTIONAL;
|
||||
|
||||
EXPECT_FALSE(convertOffsetsToPointers(archive, storage.size()));
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
::testing::InitGoogleTest(&argc, argv);
|
||||
return RUN_ALL_TESTS();
|
||||
}
|
||||
@@ -191,7 +191,11 @@ int main(int argc, char* argv[]) {
|
||||
uint64_t* basePointer = (uint64_t*) utils::aligned_alloc(decompSize, 8);
|
||||
ZSTD_decompress(basePointer, decompSize, archiveData, archiveSize);
|
||||
existingArchive = (ReadableArchive*) basePointer;
|
||||
convertOffsetsToPointers(existingArchive);
|
||||
if (!convertOffsetsToPointers(existingArchive, decompSize)) {
|
||||
cerr << "Failed to parse existing uberz archive" << endl;
|
||||
utils::aligned_free(basePointer);
|
||||
exit(1);
|
||||
}
|
||||
existingMaterialsCount = existingArchive->specsCount;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user