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:
Mathias Agopian
2026-04-13 21:02:27 -07:00
committed by Mathias Agopian
parent 8d16ad8882
commit e6e7c0325e
6 changed files with 192 additions and 13 deletions

View File

@@ -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);
}

View File

@@ -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()

View File

@@ -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

View File

@@ -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

View 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();
}

View File

@@ -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;
}