yams/tools/fuzzing/fuzz_compressed_block.cpp
trvon 9e44b172ae
test(fuzz): harnesses for search queries, UTF-8, text extraction, compression
- fuzz_fts5_query: sanitized MCP/CLI search text must execute as MATCH on
  an FTS5 table shaped like documents_fts, also after re-sanitizing.
- fuzz_utf8: sanitizer output is well-formed and preserves valid input; a
  UTF-8 verdict implies well-formed bytes; UTF-16/Latin-1 conversions
  produce well-formed UTF-8.
- fuzz_text_extractors: HTML/plain-text extraction, title and section
  detection on arbitrary bytes, with an output-size bound; run with
  -timeout to catch super-linear scans.
- fuzz_compressed_block: stored blocks decode within the header's size and
  -rss_limit_mb; zstd/LZMA round-trip exactly.

Seeds in generate_corpus.sh; the Forgejo fuzz-smoke lane builds and runs
them and triggers on the parsers' sources. Five minutes per harness on a
Linux x86 host after the fixes in this series: no findings.
2026-09-23 18:28:18 -06:00

79 lines
3 KiB
C++

// SPDX-License-Identifier: GPL-3.0-or-later
// Copyright 2026 YAMS Contributors
// Compressed storage blocks read back from disk or object storage (possibly corrupt).
// Mode 0: parse a stored CompressionHeader and decompress its payload with the header's
// (unchecksummed) uncompressed size; must not crash or exceed -rss_limit_mb, and a success
// must not produce more than that many bytes (the block CRC is checked by the caller). Mode 1:
// every available compressor round-trips the input exactly.
#include <yams/compression/compression_header.h>
#include <yams/compression/compressor_interface.h>
#include <algorithm>
#include <cstddef>
#include <cstdint>
#include <span>
#include <vector>
namespace {
void fuzzRequire(bool condition) {
if (!condition) {
__builtin_trap();
}
}
} // namespace
extern "C" int LLVMFuzzerTestOneInput(const std::uint8_t* data, std::size_t size) {
if (data == nullptr || size < 1 || size > std::size_t{1024} * 1024) {
return 0;
}
using namespace yams::compression;
const auto body =
std::span<const std::byte>(reinterpret_cast<const std::byte*>(data + 1), size - 1);
auto& registry = CompressionRegistry::instance();
if ((data[0] & 1U) == 0) {
auto header = CompressionHeader::parse(body);
if (!header) {
return 0;
}
const auto algorithm = static_cast<CompressionAlgorithm>(header.value().algorithm);
if (algorithm == CompressionAlgorithm::None) {
return 0; // raw payload; the block CRC, not a decoder, guards it
}
auto compressor = registry.createCompressor(algorithm);
if (!compressor || body.size() < CompressionHeader::SIZE) {
return 0;
}
const auto payload = body.subspan(CompressionHeader::SIZE);
const std::size_t expected = header.value().uncompressedSize;
auto decompressed = compressor->decompress(payload, expected);
if (decompressed) {
fuzzRequire(decompressed.value().size() <= expected);
}
return 0;
}
for (auto algorithm : {CompressionAlgorithm::Zstandard, CompressionAlgorithm::LZMA}) {
auto compressor = registry.createCompressor(algorithm);
if (!compressor || body.empty()) {
continue;
}
auto compressed = compressor->compress(body);
fuzzRequire(compressed.has_value());
if (compressed.value().algorithm == CompressionAlgorithm::None) {
// Incompressible input is stored raw and tagged None; callers dispatch on the tag.
fuzzRequire(std::equal(compressed.value().data.begin(), compressed.value().data.end(),
body.begin(), body.end()));
continue;
}
auto restored = compressor->decompress(compressed.value().data, body.size());
fuzzRequire(restored.has_value());
fuzzRequire(restored.value().size() == body.size());
fuzzRequire(std::equal(restored.value().begin(), restored.value().end(), body.begin()));
}
return 0;
}