diff --git a/libs/gltfio/src/ArchiveCache.cpp b/libs/gltfio/src/ArchiveCache.cpp index 18280b01b7..904c872b81 100644 --- a/libs/gltfio/src/ArchiveCache.cpp +++ b/libs/gltfio/src/ArchiveCache.cpp @@ -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(mArchive->specsCount, nullptr); } diff --git a/libs/uberz/CMakeLists.txt b/libs/uberz/CMakeLists.txt index 47744e2253..c5347796b7 100644 --- a/libs/uberz/CMakeLists.txt +++ b/libs/uberz/CMakeLists.txt @@ -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() diff --git a/libs/uberz/include/uberz/ReadableArchive.h b/libs/uberz/include/uberz/ReadableArchive.h index 5d78cb2693..2703214a6e 100644 --- a/libs/uberz/include/uberz/ReadableArchive.h +++ b/libs/uberz/include/uberz/ReadableArchive.h @@ -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 diff --git a/libs/uberz/src/ReadableArchive.cpp b/libs/uberz/src/ReadableArchive.cpp index 30ce30330a..3d04bcba74 100644 --- a/libs/uberz/src/ReadableArchive.cpp +++ b/libs/uberz/src/ReadableArchive.cpp @@ -16,7 +16,10 @@ #include -#include +#include + +#include +#include 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::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(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(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(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(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 diff --git a/libs/uberz/tests/test_ReadableArchive.cpp b/libs/uberz/tests/test_ReadableArchive.cpp new file mode 100644 index 0000000000..c9ca2cdba5 --- /dev/null +++ b/libs/uberz/tests/test_ReadableArchive.cpp @@ -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 + +#include + +#include +#include + +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 storage {}; + auto* archive = reinterpret_cast(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 storage {}; + auto* archive = reinterpret_cast(storage.data()); + archive->magic = 'UBER'; + archive->version = 0; + + archive->specsCount = 1; + archive->specsOffset = sizeof(ReadableArchive); + + auto* spec = reinterpret_cast(storage.data() + archive->specsOffset); + *spec = {}; + spec->flagsCount = 1; + spec->flagsOffset = archive->specsOffset + sizeof(ArchiveSpec); + spec->packageOffset = storage.size() - 1; + + auto* flag = reinterpret_cast(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(); +} diff --git a/tools/uberz/src/main.cpp b/tools/uberz/src/main.cpp index 45e51c8af3..1582f1cce1 100644 --- a/tools/uberz/src/main.cpp +++ b/tools/uberz/src/main.cpp @@ -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; }