fix: limit the chunks and the inflated size of an sd0 buffer

An sd0 buffer could hold any number of chunks, and each chunk grew the
inflated data by a full chunk whatever it held, so the data of a chunk
that was not full was followed by a gap. A buffer now holds at most
4096 chunks and inflates to at most 64 MiB, and every chunk is inflated
right after the one before it.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
This commit is contained in:
Aaron Kimbrell
2026-11-10 13:03:24 -06:00
parent a235e57975
commit 43cea3a245
3 changed files with 103 additions and 1 deletions

View File

@@ -66,6 +66,11 @@ Sd0::Sd0(std::istream& buffer) {
LOG("Failed to read chunk size from stream %lld %zu", buffer.tellg(), m_Chunks.size());
break;
}
if (m_Chunks.size() >= MAX_CHUNKS) {
LOG("More than %u chunks in an sd0 buffer, rejecting.", MAX_CHUNKS);
m_Chunks.clear();
break;
}
auto& chunk = m_Chunks.emplace_back();
bool firstBuffer = m_Chunks.size() == 1;
auto dataOffset = GetDataOffset(firstBuffer);
@@ -155,7 +160,8 @@ std::string Sd0::GetAsStringUncompressed() const {
break;
}
auto oldSize = toReturn.size();
// Each chunk is inflated right after the one before it
const auto oldSize = toReturn.size();
toReturn.resize(oldSize + MAX_UNCOMPRESSED_CHUNK_SIZE);
int32_t error{};
const auto uncompressedSize = ZCompression::Decompress(
@@ -171,6 +177,14 @@ std::string Sd0::GetAsStringUncompressed() const {
}
totalSize += uncompressedSize;
toReturn.resize(totalSize);
if (totalSize > MAX_UNCOMPRESSED_SIZE) {
LOG("An sd0 buffer inflates to more than %u bytes, aborting", MAX_UNCOMPRESSED_SIZE);
toReturn = "";
totalSize = 0;
break;
}
}
toReturn.resize(totalSize);

View File

@@ -4,7 +4,9 @@
#ifndef SD0_H
#define SD0_H
#include <cstdint>
#include <fstream>
#include <sstream>
#include <string>
#include <string_view>
#include <vector>
@@ -23,6 +25,16 @@ public:
*/
static constexpr inline size_t MAX_UNCOMPRESSED_CHUNK_SIZE = 1024 * 256;
/**
* @brief Max number of zlib chunks in one sd0 buffer
*/
static constexpr inline uint32_t MAX_CHUNKS = 4096;
/**
* @brief Max size of an sd0 buffer once all of its chunks are inflated, far above the size of a model
*/
static constexpr inline uint32_t MAX_UNCOMPRESSED_SIZE = 64 * 1024 * 1024;
// Empty, for FromData
Sd0() = default;

View File

@@ -4,7 +4,10 @@
#include <random>
#include <sstream>
#include <string>
#include <vector>
#include "Game.h"
#include "Logger.h"
#include "Sd0.h"
#include "ZCompression.h"
@@ -65,3 +68,76 @@ TEST(Sd0, EmptyDataIsTheHeaderOnly) {
Sd0 empty;
EXPECT_TRUE(empty.GetAsVector().empty());
}
class Sd0Test : public ::testing::Test {
protected:
void SetUp() override {
Game::logger = new Logger("./testing.log", true, true);
}
void TearDown() override {
delete Game::logger;
Game::logger = nullptr;
}
// One zlib chunk as it sits in an sd0 buffer: its size and then its data
static std::string MakeChunk(const std::string& data) {
std::vector<uint8_t> compressed(ZCompression::GetMaxCompressedLength(data.size()));
const auto compressedSize = ZCompression::Compress(
reinterpret_cast<const uint8_t*>(data.data()), data.size(),
compressed.data(), compressed.size());
EXPECT_NE(compressedSize, -1);
std::string chunk;
const uint32_t size = compressedSize;
chunk.append(reinterpret_cast<const char*>(&size), sizeof(size));
chunk.append(reinterpret_cast<const char*>(compressed.data()), size);
return chunk;
}
static std::string Inflate(const std::string& sd0Data) {
std::istringstream stream(sd0Data);
const Sd0 sd0(stream);
return sd0.GetAsStringUncompressed();
}
const std::string m_Header = std::string(Sd0::SD0_HEADER, 5);
};
TEST_F(Sd0Test, DataOfSeveralChunksSurvivesARoundTrip) {
std::string data;
for (uint32_t i = 0; data.size() < Sd0::MAX_UNCOMPRESSED_CHUNK_SIZE * 2 + 1000; i++) data += std::to_string(i) + ",";
std::istringstream empty;
Sd0 sd0(empty);
sd0.FromData(reinterpret_cast<const uint8_t*>(data.data()), data.size());
EXPECT_EQ(sd0.GetAsVector().size(), 3);
EXPECT_EQ(sd0.GetAsStringUncompressed(), data);
EXPECT_EQ(Inflate(sd0.GetAsStream().str()), data);
}
TEST_F(Sd0Test, ChunksThatAreNotFullFollowOneAnother) {
EXPECT_EQ(Inflate(m_Header + MakeChunk("abc") + MakeChunk("def") + MakeChunk("ghi")), "abcdefghi");
}
TEST_F(Sd0Test, TooManyChunksAreRefused) {
const auto chunk = MakeChunk("a");
std::string allowed = m_Header;
for (uint32_t i = 0; i < Sd0::MAX_CHUNKS; i++) allowed += chunk;
EXPECT_EQ(Inflate(allowed), std::string(Sd0::MAX_CHUNKS, 'a'));
EXPECT_EQ(Inflate(allowed + chunk), "");
}
TEST_F(Sd0Test, TooMuchInflatedDataIsRefused) {
const auto chunk = MakeChunk(std::string(Sd0::MAX_UNCOMPRESSED_CHUNK_SIZE, 'a'));
const uint32_t chunksToFill = Sd0::MAX_UNCOMPRESSED_SIZE / Sd0::MAX_UNCOMPRESSED_CHUNK_SIZE;
std::string allowed = m_Header;
for (uint32_t i = 0; i < chunksToFill; i++) allowed += chunk;
EXPECT_EQ(Inflate(allowed).size(), Sd0::MAX_UNCOMPRESSED_SIZE);
EXPECT_EQ(Inflate(allowed + MakeChunk("a")), "");
}