Files
vdm/tools/fuzz/veloxpart_fuzz.cpp
T
samiandClaude Sonnet 5 092e99f7a0 core: meta/veloxpart — resume sidecar, reader first + fuzzed (stage 5)
util/crc32.hpp — header-only CRC-32 (zlib polynomial, reflected), used to
integrity-check the sidecar.

meta/veloxpart — the <name>.veloxpart.meta resume file (docs/04 §5).
Little-endian, versioned, CRC-32 over the whole record. Layout: magic,
version, flags, total_size, downloaded, url set (original/effective/
mirrors), etag/last-modified/content-type, segment records (start, end
INCLUSIVE, completed), optional sha256 streaming-hash blob.

parse_veloxpart() is the attacker-facing surface (the file sits in a
world-writable-ish download dir) and is total on any byte string: CRC
checked before any field is interpreted; magic, a version it understands,
every count and length bounded by a hard cap AND checked against the
remaining buffer; ByteReader latches on overrun; trailing bytes rejected.
Every malformation is meta_corrupt / meta_version_unsupported, never a
crash or an unbounded allocation. serialize_veloxpart() is deterministic
(unchanged sidecar isn't rewritten). File helpers write atomically
(temp + rename) and fdatasync the file and its directory.

Tests: crc32 known vector; full + minimal round-trips; deterministic
serialize; file round-trip; and a truncation/corruption table — bad
magic, CRC mismatch (payload and CRC-field flips), future version,
truncation at every stage, hostile url_count / segment_count / lp_string
length (the case the brief singles out), trailing bytes, impossible
segment.completed.

tools/fuzz/fuzz_veloxpart — feeds raw bytes and bytes-with-valid-CRC
(so the field parser and ByteReader bounds checks are actually reached),
and round-trip-stability-checks anything accepted. Ran 1.1M execs clean
under ASan+UBSan+libFuzzer (clang++-21); fuzz_content_disposition and
fuzz_url likewise re-run to 1.1M. tools/fuzz gains a -runs=0 seed-replay
CTest smoke per target (regression tripwire; the campaign stays manual).

Fuzz-found and fixed: parse_content_disposition could emit a filename
containing NUL / control bytes from a mangled filename* ext-value —
strip_path only removed path separators. Now sanitize_leaf() also drops
C0 controls and DEL (rules/ still owns the authoritative sanitize; `..`
and printable-unsafe content pass through as before).

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01HPPSGhiArbvQgwC2DNiURS
2026-09-10 13:52:34 +04:00

53 lines
2.0 KiB
C++

// Fuzz target for the .veloxpart.meta reader — the file AGENT-CORE §5 calls
// attacker-adjacent (it lives in a world-writable-ish download directory). The reader
// must be total: no crash, no over-read, no unbounded allocation, on ANY byte string.
//
// clang++ -std=c++23 -fsanitize=fuzzer,address,undefined ... (see CMakeLists.txt)
// ./fuzz_veloxpart -max_len=8192 corpus/veloxpart/
#include <cstddef>
#include <cstdint>
#include <vector>
#include "vdm/meta/veloxpart.hpp"
#include "vdm/util/bytes.hpp"
#include "vdm/util/crc32.hpp"
using vdm::ConstByteSpan;
using vdm::meta::parse_veloxpart;
using vdm::meta::serialize_veloxpart;
using vdm::meta::VeloxPart;
namespace {
void check_roundtrip_stable(const VeloxPart &vp) {
// A value the reader accepted must serialize and re-parse to an equal value —
// otherwise the reader is accepting something the writer can't reproduce.
auto image = serialize_veloxpart(vp);
auto again = parse_veloxpart(ConstByteSpan(image.data(), image.size()));
if (!again.has_value() || !(again.value() == vp))
__builtin_trap();
}
} // namespace
extern "C" int LLVMFuzzerTestOneInput(const std::uint8_t *data, std::size_t size) {
ConstByteSpan raw(reinterpret_cast<const std::byte *>(data), size);
// 1. Raw bytes straight in — most inputs die at the magic or CRC check.
if (auto r = parse_veloxpart(raw); r.has_value())
check_roundtrip_stable(r.value());
// 2. Same bytes with a valid CRC-32 appended, so the field parser is actually
// reached and the ByteReader bounds checks (and load_le's precondition behind
// them) get exercised on structurally-plausible-but-hostile input.
std::vector<std::byte> with_crc(raw.begin(), raw.end());
std::uint32_t c = vdm::crc32(raw);
for (int i = 0; i < 4; ++i)
with_crc.push_back(static_cast<std::byte>((c >> (8 * i)) & 0xFF));
if (auto r = parse_veloxpart(ConstByteSpan(with_crc.data(), with_crc.size())); r.has_value())
check_roundtrip_stable(r.value());
return 0;
}