blob: 50201eb94d9632aa881b5bae84176a3b35a0de58 [file]
/*
*
* Copyright (c) 2026 Project CHIP Authors
* All rights reserved.
*
* 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.
*/
/**
* @file
* Stateful FuzzTest harness for the BDX TransferSession state machine.
*
* The existing TestBdxTransferSession unit tests drive two cooperating
* sessions through the happy path (and a handful of hand-crafted
* Accept/Query messages). No fuzzer drives the state machine itself: the
* 6 BdxMessages parsers were fuzzed in isolation, but the ordering /
* value-range space of HandleMessageReceived -> Handle{TransferInit,
* ReceiveAccept,SendAccept,Block,BlockEOF,BlockQuery,BlockQueryWithSkip,
* BlockAck,BlockAckEOF,StatusReport} across every TransferState was not.
*
* This harness stands up ONE TransferSession, bootstrapped as
* either an initiator (StartTransfer) or a responder (WaitForTransfer),
* then feeds it a fuzzer-chosen sequence of steps. Each step either injects
* a wire message (built from the real BdxMessages structs, so it parses and
* reaches the handler bodies) or invokes a local producer API
* (AcceptTransfer / PrepareBlock / ...). Counter-bearing messages can carry
* either the session's currently-expected counter (mirrored from the public
* getters, so the mutator gets past the counter gate into the deep
* block-processing states) or a fuzzer-chosen counter (to exercise the
* reject paths). PollOutput is drained after every step.
*
* Property: no ordering / value of BDX or StatusReport messages may crash
* the session (ASan/UBSan). The harness asserts nothing about the outputs.
*/
#include <cstdint>
#include <tuple>
#include <utility>
#include <vector>
#include <pw_fuzzer/fuzztest.h>
#include <pw_unit_test/framework.h>
#include <lib/core/CHIPError.h>
#include <lib/support/BitFlags.h>
#include <lib/support/BufferWriter.h>
#include <lib/support/CHIPMem.h>
#include <lib/support/CodeUtils.h>
#include <protocols/Protocols.h>
#include <protocols/bdx/BdxMessages.h>
#include <protocols/bdx/BdxTransferSession.h>
#include <protocols/bdx/StatusCode.h>
#include <protocols/secure_channel/Constants.h>
#include <system/SystemClock.h>
#include <system/SystemPacketBuffer.h>
#include <transport/raw/MessageHeader.h>
namespace {
using namespace chip;
using namespace chip::bdx;
using namespace fuzztest;
using chip::System::PacketBufferHandle;
constexpr System::Clock::Timestamp kNoAdvanceTime = System::Clock::kZero;
// Upper bound on the number of steps per sequence (template arg to pw::fuzzer::VectorOf).
constexpr size_t kMaxSteps = 24;
void EnsureInitialized()
{
static const bool sInitialized = [] {
VerifyOrDie(chip::Platform::MemoryInit() == CHIP_NO_ERROR);
return true;
}();
(void) sInitialized;
}
// Serialize a BdxMessages struct into a PacketBuffer, matching the injection idiom
// used by TestBdxTransferSession. MessageSize() and WriteToBuffer() share a code
// path, so the allocated buffer is always exactly the needed size.
template <typename MsgType>
PacketBufferHandle EncodeBdxMessage(const MsgType & msg)
{
const size_t msgSize = msg.MessageSize();
Encoding::LittleEndian::PacketBufferWriter writer(PacketBufferHandle::New(msgSize), msgSize);
if (writer.IsNull())
{
return PacketBufferHandle();
}
msg.WriteToBuffer(writer);
return writer.Finalize();
}
// Wrap a payload in a PayloadHeader and feed it to the session, exactly as the
// exchange layer would once it has stripped the transport header.
void Inject(TransferSession & session, Protocols::Id protocol, uint8_t msgType, PacketBufferHandle buf)
{
if (buf.IsNull())
{
return;
}
PayloadHeader header;
header.SetMessageType(protocol, msgType);
RETURN_SAFELY_IGNORED session.HandleMessageReceived(header, std::move(buf), kNoAdvanceTime);
}
// Build a PacketBuffer from raw bytes. An empty payload maps to an empty
// buffer via New(0) rather than NewWithData(payload.data(), 0), since an empty
// std::vector's data() may be null.
PacketBufferHandle MakeBuf(const std::vector<uint8_t> & payload)
{
if (payload.empty())
{
return PacketBufferHandle::New(0);
}
return PacketBufferHandle::NewWithData(payload.data(), payload.size());
}
void DrainOutput(TransferSession & session)
{
for (int i = 0; i < 8; ++i)
{
TransferSession::OutputEvent event;
session.PollOutput(event, kNoAdvanceTime);
if (event.EventType == TransferSession::OutputEventType::kNone)
{
break;
}
}
}
// Steps. The low bits of `opcode` select the action; the high bit selects whether
// counter-bearing messages carry the mirrored (expected) counter or the fuzzed one.
enum class Op : uint8_t
{
kInjectInit = 0,
kInjectReceiveAccept,
kInjectSendAccept,
kInjectBlockQuery,
kInjectBlockQueryWithSkip,
kInjectBlock,
kInjectBlockEOF,
kInjectBlockAck,
kInjectBlockAckEOF,
kInjectRawStatusReport,
kInjectRawBdx,
kLocalAccept,
kLocalReject,
kLocalPrepareBlockQuery,
kLocalPrepareBlockQueryWithSkip,
kLocalPrepareBlock,
kLocalPrepareBlockAck,
kLocalAbort,
kDrain,
kOpCount,
};
// Per-step fuzzed fields: (opcode, u16, u32, u64, payload).
using Step = std::tuple<uint8_t, uint16_t, uint32_t, uint64_t, std::vector<uint8_t>>;
const TransferControlFlags kDriveModes[] = { TransferControlFlags::kSenderDrive, TransferControlFlags::kReceiverDrive,
TransferControlFlags::kAsync };
StatusCode PickStatusCode(uint32_t v)
{
const StatusCode codes[] = { StatusCode::kLengthTooLarge,
StatusCode::kLengthMismatch,
StatusCode::kBadMessageContents,
StatusCode::kBadBlockCounter,
StatusCode::kUnexpectedMessage,
StatusCode::kTransferFailedUnknownError,
StatusCode::kTransferMethodNotSupported,
StatusCode::kFileDesignatorUnknown,
StatusCode::kStartOffsetNotSupported,
StatusCode::kVersionNotSupported,
StatusCode::kUnknown };
return codes[v % (sizeof(codes) / sizeof(codes[0]))];
}
void HandleStep(TransferSession & session, const Step & step)
{
const uint8_t rawOp = std::get<0>(step);
const uint16_t u16 = std::get<1>(step);
const uint32_t u32 = std::get<2>(step);
const uint64_t u64 = std::get<3>(step);
const std::vector<uint8_t> & payload = std::get<4>(step);
const Op op = static_cast<Op>(rawOp % static_cast<uint8_t>(Op::kOpCount));
// Derive the mirror flag from a value bit (NOT from rawOp: `rawOp % kOpCount`
// consumes the whole byte, so a high bit there would perturb the opcode).
const bool useMirror = (u32 & 0x1u) != 0u;
switch (op)
{
case Op::kInjectInit: {
TransferInit msg;
msg.TransferCtlOptions.Set(kDriveModes[u16 % 3]);
msg.Version = 0;
msg.MaxBlockSize = static_cast<uint16_t>(u32);
msg.StartOffset = u64;
msg.MaxLength = (static_cast<uint64_t>(u32) << 8) ^ u64;
msg.FileDesLength = static_cast<uint16_t>(payload.size() > kMaxFileDesignatorLen ? kMaxFileDesignatorLen : payload.size());
msg.FileDesignator = payload.empty() ? nullptr : payload.data();
const MessageType type = (u32 & 0x1u) ? MessageType::SendInit : MessageType::ReceiveInit;
Inject(session, Protocols::BDX::Id, to_underlying(type), EncodeBdxMessage(msg));
break;
}
case Op::kInjectReceiveAccept: {
ReceiveAccept msg;
msg.TransferCtlFlags.Set(kDriveModes[u16 % 3]);
msg.Version = 0;
msg.MaxBlockSize = static_cast<uint16_t>(u16);
msg.StartOffset = static_cast<uint64_t>(u32);
msg.Length = u64;
Inject(session, Protocols::BDX::Id, to_underlying(MessageType::ReceiveAccept), EncodeBdxMessage(msg));
break;
}
case Op::kInjectSendAccept: {
SendAccept msg;
msg.TransferCtlFlags.Set(kDriveModes[u16 % 3]);
msg.Version = 0;
msg.MaxBlockSize = static_cast<uint16_t>(u16);
Inject(session, Protocols::BDX::Id, to_underlying(MessageType::SendAccept), EncodeBdxMessage(msg));
break;
}
case Op::kInjectBlockQuery: {
BlockQuery msg;
msg.BlockCounter = useMirror ? session.GetNextBlockNum() : u32;
Inject(session, Protocols::BDX::Id, to_underlying(MessageType::BlockQuery), EncodeBdxMessage(msg));
break;
}
case Op::kInjectBlockQueryWithSkip: {
BlockQueryWithSkip msg;
msg.BlockCounter = useMirror ? session.GetNextBlockNum() : u32;
msg.BytesToSkip = u64;
Inject(session, Protocols::BDX::Id, to_underlying(MessageType::BlockQueryWithSkip), EncodeBdxMessage(msg));
break;
}
case Op::kInjectBlock: {
Block msg;
// HandleBlock checks the incoming counter against mLastQueryNum (the query the receiver last sent).
msg.BlockCounter = useMirror ? session.GetLastQueryNum() : u32;
msg.Data = payload.empty() ? nullptr : payload.data();
msg.DataLength = payload.size();
Inject(session, Protocols::BDX::Id, to_underlying(MessageType::Block), EncodeBdxMessage(msg));
break;
}
case Op::kInjectBlockEOF: {
BlockEOF msg;
msg.BlockCounter = useMirror ? session.GetLastQueryNum() : u32;
msg.Data = payload.empty() ? nullptr : payload.data();
msg.DataLength = payload.size();
Inject(session, Protocols::BDX::Id, to_underlying(MessageType::BlockEOF), EncodeBdxMessage(msg));
break;
}
case Op::kInjectBlockAck: {
BlockAck msg;
// HandleBlockAck checks the incoming counter against mLastBlockNum (the block the sender last sent).
msg.BlockCounter = useMirror ? session.GetLastBlockNum() : u32;
Inject(session, Protocols::BDX::Id, to_underlying(MessageType::BlockAck), EncodeBdxMessage(msg));
break;
}
case Op::kInjectBlockAckEOF: {
BlockAckEOF msg;
msg.BlockCounter = useMirror ? session.GetLastBlockNum() : u32;
Inject(session, Protocols::BDX::Id, to_underlying(MessageType::BlockAckEOF), EncodeBdxMessage(msg));
break;
}
case Op::kInjectRawStatusReport: {
// Raw SecureChannel StatusReport bytes -> HandleStatusReportMessage / StatusReport::Parse.
Inject(session, Protocols::SecureChannel::Id, to_underlying(Protocols::SecureChannel::MsgType::StatusReport),
MakeBuf(payload));
break;
}
case Op::kInjectRawBdx: {
// Arbitrary bytes under a fuzzer-chosen BDX message type -> the Handle*/Parse dispatch
// with garbage, in whatever state the session currently holds.
Inject(session, Protocols::BDX::Id, static_cast<uint8_t>(u16), MakeBuf(payload));
break;
}
case Op::kLocalAccept: {
TransferSession::TransferAcceptData acceptData;
acceptData.ControlMode = kDriveModes[u16 % 3];
acceptData.MaxBlockSize = static_cast<uint16_t>(u16);
acceptData.StartOffset = static_cast<uint64_t>(u32);
acceptData.Length = u64;
RETURN_SAFELY_IGNORED session.AcceptTransfer(acceptData);
break;
}
case Op::kLocalReject: {
RETURN_SAFELY_IGNORED session.RejectTransfer(PickStatusCode(u32));
break;
}
case Op::kLocalPrepareBlockQuery: {
RETURN_SAFELY_IGNORED session.PrepareBlockQuery();
break;
}
case Op::kLocalPrepareBlockQueryWithSkip: {
RETURN_SAFELY_IGNORED session.PrepareBlockQueryWithSkip(u64);
break;
}
case Op::kLocalPrepareBlock: {
TransferSession::BlockData blockData;
blockData.Data = payload.empty() ? nullptr : payload.data();
blockData.Length = payload.size();
blockData.IsEof = (u32 & 0x1u) != 0u;
RETURN_SAFELY_IGNORED session.PrepareBlock(blockData);
break;
}
case Op::kLocalPrepareBlockAck: {
RETURN_SAFELY_IGNORED session.PrepareBlockAck();
break;
}
case Op::kLocalAbort: {
RETURN_SAFELY_IGNORED session.AbortTransfer(PickStatusCode(u32));
break;
}
case Op::kDrain:
case Op::kOpCount:
break;
}
DrainOutput(session);
}
Step Mk(uint8_t op, uint16_t a, uint32_t b, uint64_t c, std::vector<uint8_t> payload = {})
{
return std::make_tuple(op, a, b, c, std::move(payload));
}
// A complete FUZZ_TEST input: the six setup scalars plus the step vector. The seeds below
// are whole-input seeds so each step sequence is pinned to the setup role / drive-mode that
// lets it reach the deep in-progress handlers. (Seeding only the step vector -- as before --
// left weInitiate/role/drive-mode random, so a deep sequence fired only when the random setup
// happened to match, and the block-prepare / ack handlers stayed near-uncovered.)
using SessionInput = std::tuple<bool, uint8_t, uint8_t, uint16_t, uint64_t, uint64_t, std::vector<Step>>;
SessionInput MkInput(bool weInitiate, uint8_t roleByte, uint8_t initFlags, uint16_t initMaxBlock, uint64_t initLength,
uint64_t initOffset, std::vector<Step> steps)
{
return std::make_tuple(weInitiate, roleByte, initFlags, initMaxBlock, initLength, initOffset, std::move(steps));
}
// Seeds that walk ONE session through the accept -> query -> block(/EOF) -> ack flow for each
// role x drive-mode, so the mutator starts inside kTransferInProgress instead of rediscovering
// the exact (role, drive-mode, ordered messages, matching counters) tuple from random bytes.
// roleByte: even = receiver, odd = sender. initFlags % 3: 0 = SenderDrive, 1 = ReceiverDrive.
// For counter-bearing steps, u32 bit0 = 1 requests the session's currently-expected (mirrored)
// counter. A Block/Ack whose expected counter is odd cannot use the mirror (bit0 doubles as the
// mirror flag), and the mirror only matches for the first block of a transfer, so these seeds
// stay single-cycle and use non-mirror even literals for acks (mLastBlockNum == 0 after one block).
std::vector<SessionInput> BdxWholeInputSeeds()
{
const std::vector<uint8_t> blk(64, 0xAB);
const std::vector<uint8_t> blk32(32, 0xCD);
const std::vector<uint8_t> fileDes = { 't', 'e', 's', 't', '.', 'b', 'i', 'n' };
return {
// Initiating receiver, ReceiverDrive: ReceiveAccept -> BlockQuery -> Block -> PrepareBlockAck.
MkInput(true, 0, 1, 64, 200, 0, { Mk(1, 64, 0, 200, {}), Mk(13, 0, 0, 0), Mk(5, 0, 1, 0, blk), Mk(16, 0, 0, 0) }),
// Initiating receiver, ReceiverDrive, EOF path: ReceiveAccept -> BlockQuery -> BlockEOF -> PrepareBlockAck(EOF).
MkInput(true, 0, 1, 64, 200, 0, { Mk(1, 64, 0, 200, {}), Mk(13, 0, 0, 0), Mk(6, 0, 1, 0, blk), Mk(16, 0, 0, 0) }),
// Initiating receiver, ReceiverDrive, skip: ReceiveAccept -> PrepareBlockQueryWithSkip.
MkInput(true, 0, 1, 64, 0, 0, { Mk(1, 64, 0, 0, {}), Mk(14, 0, 0, 128) }),
// Initiating receiver, SenderDrive: ReceiveAccept -> Block -> PrepareBlockAck.
MkInput(true, 0, 0, 64, 200, 0, { Mk(1, 63, 0, 200, {}), Mk(5, 0, 1, 0, blk32), Mk(16, 0, 0, 0) }),
// Initiating sender, SenderDrive: SendAccept -> PrepareBlock immediately (no query needed).
MkInput(true, 1, 0, 64, 0, 0, { Mk(2, 63, 0, 0, {}), Mk(15, 0, 0, 0, blk32) }),
// Initiating sender, SenderDrive, EOF: SendAccept -> PrepareBlock(EOF) -> BlockAckEOF(counter 0).
MkInput(true, 1, 0, 64, 0, 0, { Mk(2, 63, 0, 0, {}), Mk(15, 0, 1, 0, blk32), Mk(8, 0, 0, 0) }),
// Initiating sender, ReceiverDrive: SendAccept -> BlockQuery(mirror) -> PrepareBlock -> BlockAck(counter 0).
MkInput(true, 1, 1, 64, 0, 0, { Mk(2, 64, 0, 0, {}), Mk(3, 0, 1, 0), Mk(15, 0, 0, 0, blk), Mk(7, 0, 0, 0) }),
// Initiating sender, ReceiverDrive: SendAccept -> BlockQueryWithSkip(mirror) -> PrepareBlock (covers
// HandleBlockQueryWithSkip).
MkInput(true, 1, 1, 64, 0, 0, { Mk(2, 64, 0, 0, {}), Mk(4, 0, 1, 0), Mk(15, 0, 0, 0, blk) }),
// Responder as sender, ReceiverDrive: ReceiveInit -> local Accept -> BlockQuery(mirror) -> PrepareBlock.
// op0 u32 carries the ReceiveInit MaxBlockSize (64); the local Accept's block size must not exceed it.
MkInput(false, 1, 1, 64, 0, 0, { Mk(0, 64, 64, 0, fileDes), Mk(11, 64, 0, 0), Mk(3, 0, 1, 0), Mk(15, 0, 0, 0, blk) }),
// Responder as receiver, ReceiverDrive: SendInit -> local Accept (SendAccept branch) -> PrepareBlockQuery.
MkInput(false, 0, 1, 64, 0, 0, { Mk(0, 64, 65, 0, fileDes), Mk(11, 64, 0, 0), Mk(13, 0, 0, 0) }),
// Responder reject: ReceiveInit -> local Reject.
MkInput(false, 1, 0, 64, 0, 0, { Mk(0, 64, 0, 0, fileDes), Mk(12, 0, 0, 0) }),
// Initiating receiver, ReceiverDrive, MULTI-BLOCK: Accept(len 1024) then 8x (Query, Block) then Query, BlockEOF.
// Each Block/BlockEOF uses the mirrored counter; this only progresses past block 1 if the mirror is correct.
MkInput(true, 0, 1, 64, 1024, 0,
{ Mk(1, 64, 0, 1024, {}), Mk(13, 0, 0, 0), Mk(5, 0, 1, 0, blk), Mk(13, 0, 0, 0), Mk(5, 0, 1, 0, blk),
Mk(13, 0, 0, 0), Mk(5, 0, 1, 0, blk), Mk(13, 0, 0, 0), Mk(5, 0, 1, 0, blk), Mk(13, 0, 0, 0), Mk(5, 0, 1, 0, blk),
Mk(13, 0, 0, 0), Mk(5, 0, 1, 0, blk), Mk(13, 0, 0, 0), Mk(5, 0, 1, 0, blk), Mk(13, 0, 0, 0), Mk(5, 0, 1, 0, blk),
Mk(13, 0, 0, 0), Mk(6, 0, 1, 0, blk) }),
};
}
void BdxSessionSequenceDoesNotCrash(bool weInitiate, uint8_t roleByte, uint8_t initFlags, uint16_t initMaxBlock,
uint64_t initLength, uint64_t initOffset, const std::vector<Step> & steps)
{
EnsureInitialized();
TransferSession session;
const System::Clock::Timeout timeout = System::Clock::Seconds16(24);
const TransferRole role = (roleByte & 0x1u) ? TransferRole::kSender : TransferRole::kReceiver;
const uint16_t setupBlock = (initMaxBlock == 0) ? 64 : initMaxBlock;
if (weInitiate)
{
TransferSession::TransferInitData initData;
initData.TransferCtlFlags = kDriveModes[initFlags % 3];
initData.MaxBlockSize = setupBlock;
initData.StartOffset = initOffset;
initData.Length = initLength;
static const uint8_t kFileDes[] = { 't', 'e', 's', 't', '.', 'b', 'i', 'n' };
initData.FileDesignator = kFileDes;
initData.FileDesLength = static_cast<uint16_t>(sizeof(kFileDes));
if (session.StartTransfer(role, initData, timeout) != CHIP_NO_ERROR)
{
return;
}
}
else
{
BitFlags<TransferControlFlags> opts;
opts.Set(TransferControlFlags::kSenderDrive).Set(TransferControlFlags::kReceiverDrive);
if (session.WaitForTransfer(role, opts, setupBlock, timeout) != CHIP_NO_ERROR)
{
return;
}
}
DrainOutput(session);
for (const Step & step : steps)
{
HandleStep(session, step);
}
}
// The seven parameter domains are wrapped in ONE TupleOf so the whole-input seeds attach at DOMAIN
// level (WithDomains unpacks a tuple-aggregate domain onto the parameter list). Seeds chained on
// the FUZZ_TEST registration itself are only loaded through the runtime's seed enumeration, which
// the libFuzzer-compat mode used by OSS-Fuzz never runs -- domain-level seeds ride the domain's
// Init() and survive there.
FUZZ_TEST(FuzzBdxTransferSessionPW, BdxSessionSequenceDoesNotCrash)
.WithDomains(TupleOf(Arbitrary<bool>(), // weInitiate: StartTransfer vs WaitForTransfer
Arbitrary<uint8_t>(), // role selector
Arbitrary<uint8_t>(), // drive-mode selector for setup
Arbitrary<uint16_t>(), // proposed MaxBlockSize
Arbitrary<uint64_t>(), // proposed transfer length
Arbitrary<uint64_t>(), // proposed start offset
VectorOf(TupleOf(Arbitrary<uint8_t>(), Arbitrary<uint16_t>(), Arbitrary<uint32_t>(), Arbitrary<uint64_t>(),
Arbitrary<std::vector<uint8_t>>()))
.WithMaxSize(kMaxSteps))
.WithSeeds(BdxWholeInputSeeds()));
} // namespace