blob: de6bd99e913081f4e01b9ac69198112e6c65c517 [file] [log] [blame] [edit]
// Copyright 2023 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include <lib/storage/gpt_utils.h>
#include <lib/storage/sparse.h>
#include <lib/storage/storage.h>
#include <sparse_format.h>
#include <algorithm>
#include <array>
#include <numeric>
#include <optional>
#include <span>
#include <vector>
#include <gtest/gtest.h>
#include "src/firmware/lib/zircon_boot/test/test_data/test_images.h"
#include "test_utils.h"
// In the generated gpt data, the block size is 512
// and the vbmeta_{a,b,r} partitions all have 6 blocks.
// It's easier to test using a nonstandard (but completely valid) chunk size
// to stay under the maximum partition size than to work around this in other ways.
constexpr size_t kBlkSize = 512;
struct SparseDataDescriptor {
enum class SparseChunkType {
kUnknown,
kRaw = CHUNK_TYPE_RAW,
kFill = CHUNK_TYPE_FILL,
kDontCare = CHUNK_TYPE_DONT_CARE,
kCrC32 = CHUNK_TYPE_CRC32,
};
SparseChunkType type;
size_t output_blocks = 0;
uint32_t payload = 0;
template <size_t ChunkHeaderSize>
constexpr size_t total_sz() const {
static_assert(ChunkHeaderSize >= sizeof(chunk_header_t));
switch (type) {
case SparseChunkType::kRaw:
return output_blocks * kBlkSize + ChunkHeaderSize;
case SparseChunkType::kFill:
case SparseChunkType::kCrC32:
return ChunkHeaderSize + sizeof(payload);
case SparseChunkType::kDontCare:
default:
return ChunkHeaderSize;
}
}
constexpr uint32_t ExpectedImageWord(size_t word_offset, uint32_t base_fill) const {
switch (type) {
case SparseChunkType::kRaw:
return payload + static_cast<uint32_t>(word_offset);
case SparseChunkType::kFill:
return payload;
case SparseChunkType::kDontCare:
return base_fill;
default:
return 0;
}
}
};
template <typename T>
void AddBytes(std::vector<uint8_t> &vec, const T &data) {
const uint8_t *data_bytes = reinterpret_cast<const uint8_t *>(&data);
vec.insert(vec.end(), data_bytes, data_bytes + sizeof(data));
}
class GptDataHolder {
public:
explicit GptDataHolder(FuchsiaFirmwareStorage &storage) : storage_(storage) {}
~GptDataHolder() { FuchsiaFirmwareStorageFreeGptData(&storage_, &data_); }
GptData *gpt_data() { return &data_; }
private:
GptData data_;
FuchsiaFirmwareStorage &storage_;
};
template <size_t FileHeaderSize, size_t ChunkHeaderSize>
std::vector<uint8_t> MakeSparseImage(std::span<const SparseDataDescriptor> descriptors) {
static_assert(FileHeaderSize >= sizeof(sparse_header_t));
static_assert(ChunkHeaderSize >= sizeof(chunk_header_t));
std::vector<uint8_t> data;
size_t output_blocks = std::reduce(
descriptors.begin(), descriptors.end(), 0,
[](size_t sum, const SparseDataDescriptor &d) -> size_t { return sum + d.output_blocks; });
sparse_header_t header = {
.magic = SPARSE_HEADER_MAGIC,
.major_version = 1,
.file_hdr_sz = FileHeaderSize,
.chunk_hdr_sz = ChunkHeaderSize,
.blk_sz = kBlkSize,
.total_blks = static_cast<uint32_t>(output_blocks),
.total_chunks = static_cast<uint32_t>(descriptors.size()),
.image_checksum = 0xDEADBEEF // We don't do crc validation as of 2023-04-19
};
AddBytes(data, header);
constexpr size_t file_header_pad_bytes = FileHeaderSize - sizeof(sparse_header_t);
if constexpr (file_header_pad_bytes) {
std::array<uint8_t, file_header_pad_bytes> file_header_pad;
AddBytes(data, file_header_pad);
}
for (const auto &chunk : descriptors) {
chunk_header_t hdr = {
.chunk_type = static_cast<uint16_t>(chunk.type),
.reserved1 = 0,
.chunk_sz = static_cast<uint32_t>(chunk.output_blocks),
.total_sz = static_cast<uint32_t>(chunk.total_sz<ChunkHeaderSize>()),
};
AddBytes(data, hdr);
constexpr size_t chunk_header_pad_bytes = ChunkHeaderSize - sizeof(chunk_header_t);
if constexpr (chunk_header_pad_bytes) {
std::array<uint8_t, chunk_header_pad_bytes> chunk_header_pad;
AddBytes(data, chunk_header_pad);
}
// Add payload + i to differentiate raw and fill chunks.
size_t payload_words = (chunk.total_sz<ChunkHeaderSize>() - ChunkHeaderSize) / sizeof(uint32_t);
for (size_t i = 0; i < payload_words; i++) {
AddBytes<uint32_t>(data, chunk.payload + static_cast<uint32_t>(i));
}
}
return data;
}
std::vector<uint8_t> GenerateExpectedData(std::span<const SparseDataDescriptor> descriptors,
uint32_t base_fill) {
std::vector<uint8_t> data;
for (const auto &chunk : descriptors) {
for (size_t i = 0; i < (chunk.output_blocks * kBlkSize) / sizeof(chunk.payload); i++) {
AddBytes<uint32_t>(data, chunk.ExpectedImageWord(i, base_fill));
}
}
return data;
}
bool FillPartition(FuchsiaFirmwareStorage &ops, const GptData *data, const char *name,
uint32_t payload) {
size_t partition_size;
if (!FuchsiaFirmwareStorageGetPartitionSize(&ops, data, name, &partition_size)) {
return false;
}
std::vector<uint32_t> fill(partition_size / sizeof(uint32_t), payload);
return FuchsiaFirmwareStorageGptWrite(&ops, data, name, 0, fill.size() * sizeof(uint32_t),
fill.data());
}
TEST(FuchsiaSparseWriterTest, TestEmptyImage) {
// The serialized gpt header expects 512 byte blocks. We don't actually care very much,
// but it's easier to play along.
TestFuchsiaFirmwareStorage test_storage(sizeof(kTestGptDisk), 512);
memcpy(test_storage.buffer().data(), kTestGptDisk, sizeof(kTestGptDisk));
FuchsiaFirmwareStorage storage = test_storage.GetFuchsiaFirmwareStorage();
GptDataHolder gpt_data(storage);
ASSERT_TRUE(FuchsiaFirmwareStorageSyncGpt(&storage, gpt_data.gpt_data()));
// We initialize the partition to a non-zero value to make sure that we aren't
// writing zeroes.
const char *partition = "zircon_a";
ASSERT_TRUE(FillPartition(storage, gpt_data.gpt_data(), partition, 0x55555555));
std::vector<uint8_t> sparse_image =
MakeSparseImage<sizeof(sparse_header_t), sizeof(chunk_header_t)>({});
std::vector<uint8_t> before_data(kBlkSize);
ASSERT_TRUE(FuchsiaFirmwareStorageGptRead(&storage, gpt_data.gpt_data(), partition, 0,
before_data.size(), before_data.data()));
ASSERT_TRUE(FuchsiaWriteSparseImage(&storage, gpt_data.gpt_data(), partition, sparse_image.data(),
sparse_image.size()));
std::vector<uint8_t> after_data(before_data.size());
ASSERT_TRUE(FuchsiaFirmwareStorageGptRead(&storage, gpt_data.gpt_data(), partition, 0,
after_data.size(), after_data.data()));
ASSERT_EQ(before_data, after_data);
}
template <size_t FileHeaderSize, size_t ChunkHeaderSize>
void RunBasicSparseTest() {
TestFuchsiaFirmwareStorage test_storage(sizeof(kTestGptDisk), 512);
memcpy(test_storage.buffer().data(), kTestGptDisk, sizeof(kTestGptDisk));
FuchsiaFirmwareStorage storage = test_storage.GetFuchsiaFirmwareStorage();
GptDataHolder gpt_data(storage);
ASSERT_TRUE(FuchsiaFirmwareStorageSyncGpt(&storage, gpt_data.gpt_data()));
// Same as above: we particularly want to make sure DONT_CARE isn't writing zeroes.
const char *partition = "vbmeta_a";
constexpr uint32_t base_fill = 0x55555555;
ASSERT_TRUE(FillPartition(storage, gpt_data.gpt_data(), partition, base_fill));
constexpr SparseDataDescriptor chunks[] = {
{SparseDataDescriptor::SparseChunkType::kRaw, 1, 0xCAFED00D},
{SparseDataDescriptor::SparseChunkType::kDontCare, 1},
{SparseDataDescriptor::SparseChunkType::kFill, 1, 0x8BADF00D},
{SparseDataDescriptor::SparseChunkType::kCrC32, 0, 0xCAB00D1E},
{SparseDataDescriptor::SparseChunkType::kFill, 2, 0xCABBA6E5},
{SparseDataDescriptor::SparseChunkType::kRaw, 2, 0xFEEDC0DE},
{SparseDataDescriptor::SparseChunkType::kDontCare, 1},
};
std::vector<uint8_t> sparse_image = MakeSparseImage<FileHeaderSize, ChunkHeaderSize>(chunks);
std::vector<uint8_t> expected = GenerateExpectedData(chunks, base_fill);
ASSERT_TRUE(FuchsiaWriteSparseImage(&storage, gpt_data.gpt_data(), partition, sparse_image.data(),
sparse_image.size()));
std::vector<uint8_t> actual(expected.size());
ASSERT_TRUE(FuchsiaFirmwareStorageGptRead(&storage, gpt_data.gpt_data(), partition, 0,
actual.size(), actual.data()));
if (expected != actual) {
auto mismatch = std::mismatch(expected.begin(), expected.end(), actual.begin(), actual.end());
FAIL() << "Mismatch at index " << std::distance(expected.begin(), mismatch.first) << " of "
<< expected.size() << " (wanted '" << *mismatch.first << "', got '" << *mismatch.second
<< "')";
}
}
TEST(FuchsiaSparseWriterTest, TestBasic) {
RunBasicSparseTest<sizeof(sparse_header_t), sizeof(chunk_header_t)>();
}
TEST(FuchsiaSparseWriterTest, TestBasicLargeHeaders) {
RunBasicSparseTest<sizeof(sparse_header_t) * 2, sizeof(chunk_header_t) * 2>();
}
struct FuchsiaSparseWriterBadHeaderTestCase {
std::string_view name;
void (*corruption_func)(sparse_header_t &);
};
using FuchsiaSparseWriterBadHeaderTest =
::testing::TestWithParam<FuchsiaSparseWriterBadHeaderTestCase>;
TEST_P(FuchsiaSparseWriterBadHeaderTest, TestBadHeader) {
const FuchsiaSparseWriterBadHeaderTestCase &test_case = GetParam();
TestFuchsiaFirmwareStorage test_storage(sizeof(kTestGptDisk), 512);
memcpy(test_storage.buffer().data(), kTestGptDisk, sizeof(kTestGptDisk));
FuchsiaFirmwareStorage storage = test_storage.GetFuchsiaFirmwareStorage();
GptDataHolder gpt_data(storage);
ASSERT_TRUE(FuchsiaFirmwareStorageSyncGpt(&storage, gpt_data.gpt_data()));
constexpr SparseDataDescriptor chunks[] = {
{SparseDataDescriptor::SparseChunkType::kRaw, 1, 0x8BADF00D},
};
std::vector<uint8_t> sparse_image =
MakeSparseImage<sizeof(sparse_header_t), sizeof(chunk_header_t)>(chunks);
sparse_header_t *header = reinterpret_cast<sparse_header_t *>(sparse_image.data());
test_case.corruption_func(*header);
ASSERT_FALSE(FuchsiaWriteSparseImage(&storage, gpt_data.gpt_data(), "vbmeta_a",
sparse_image.data(), sparse_image.size()));
}
INSTANTIATE_TEST_SUITE_P(
FuchsiaSparseWriterBadHeaderTests, FuchsiaSparseWriterBadHeaderTest,
testing::ValuesIn<FuchsiaSparseWriterBadHeaderTestCase>({
{"bad_magic", [](sparse_header_t &h) { h.magic = 0xBEEF; }},
{"major_too_high", [](sparse_header_t &h) { h.major_version = 16; }},
{"bad_hdr_size", [](sparse_header_t &h) { h.file_hdr_sz = 0x6; }},
{"bad_chunk_size", [](sparse_header_t &h) { h.chunk_hdr_sz = 0x7; }},
{"bad_blk_size", [](sparse_header_t &h) { h.blk_sz = 511; }},
{"big_blk_size", [](sparse_header_t &h) { h.blk_sz = 8192; }},
}),
[](const testing::TestParamInfo<FuchsiaSparseWriterBadHeaderTest::ParamType> &info) {
return std::string(info.param.name.begin(), info.param.name.end());
});
struct FuchsiaSparseWriterBadChunkTestCase {
std::string_view name;
uint16_t chunk_type;
size_t payload_size;
};
using FuchsiaSparseWriterBadChunkTest =
::testing::TestWithParam<FuchsiaSparseWriterBadChunkTestCase>;
TEST_P(FuchsiaSparseWriterBadChunkTest, TestBadChunk) {
const FuchsiaSparseWriterBadChunkTestCase &test_case = GetParam();
TestFuchsiaFirmwareStorage test_storage(sizeof(kTestGptDisk), 512);
memcpy(test_storage.buffer().data(), kTestGptDisk, sizeof(kTestGptDisk));
FuchsiaFirmwareStorage storage = test_storage.GetFuchsiaFirmwareStorage();
GptDataHolder gpt_data(storage);
ASSERT_TRUE(FuchsiaFirmwareStorageSyncGpt(&storage, gpt_data.gpt_data()));
std::vector<uint8_t> sparse_image =
MakeSparseImage<sizeof(sparse_header_t), sizeof(chunk_header_t)>({});
chunk_header_t chunk = {
.chunk_type = test_case.chunk_type,
.reserved1 = 0,
.chunk_sz = 1,
.total_sz = static_cast<uint32_t>(sizeof(chunk_header_t) + test_case.payload_size),
};
AddBytes(sparse_image, chunk);
// Add a payload for raw chunk tests
for (size_t i = 0; i < kBlkSize; i++) {
sparse_image.push_back(0);
}
reinterpret_cast<sparse_header_t *>(sparse_image.data())->total_chunks = 1;
ASSERT_FALSE(FuchsiaWriteSparseImage(&storage, gpt_data.gpt_data(), "vbmeta_a",
sparse_image.data(), sparse_image.size()));
}
INSTANTIATE_TEST_SUITE_P(
FuchsiaSparseWriterBadChunkTests, FuchsiaSparseWriterBadChunkTest,
testing::ValuesIn<FuchsiaSparseWriterBadChunkTestCase>({
{"chunk_too_big_for_image", CHUNK_TYPE_FILL, 0x40000},
{"inconsistent_raw_chunk", CHUNK_TYPE_RAW, 15},
{"inconsistent_dont_care_chunk", CHUNK_TYPE_DONT_CARE, 1},
{"inconsistent_fill_chunk", CHUNK_TYPE_FILL, 0},
{"inconsistent_crc32_chunk", CHUNK_TYPE_CRC32, 0},
{"unexpected_chunk_type", 0xBEEF, 0},
}),
[](const testing::TestParamInfo<FuchsiaSparseWriterBadChunkTest::ParamType> &info) {
return std::string(info.param.name.begin(), info.param.name.end());
});