blob: 9cd377aa753f99c85e94d50cb1cb6ffb3239829a [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
* Seeded/typed FuzzTest harness for FillNodeDataFromTxt — the DNS-SD TXT
* key/value parser that populates CommonResolutionData and
* CommissionNodeData during operational and commissionable-node
* discovery.
*
* The libFuzzer variant had to carve a key and a value out of one flat
* buffer (1-byte length prefix + key + value), which meant most of the
* mutator's energy went into discovering the handful of legal two/three
* letter keys instead of stressing the per-field numeric/string parsers.
* Here the key and value are separate typed domains: the key domain is
* seeded with the exact TXT keys from TxtFields.h (so the interesting
* switch arms are hit immediately) and the value domain is seeded with
* realistic and boundary values (boundary numbers, overflow forms,
* leading zeros, oversize strings, malformed hex).
*
* Oracle (beyond no-crash): the parser must never leave a struct field
* outside the range its accessor promises —
* - MRP idle/active intervals, when present, are <= kMaxRetryInterval;
* - rotatingIdLen never exceeds the rotatingId[] buffer;
* - the fixed-size deviceName / pairingInstruction buffers stay
* NUL-terminated within bounds.
* No-crash under ASan/MSan/UBSan is the floor; these invariants are the
* ceiling.
*/
#include <cstddef>
#include <cstdint>
#include <cstring>
#include <string>
#include <vector>
#include <fuzztest/fuzztest_core.h>
#include <gtest/gtest.h>
#include <lib/dnssd/Resolver.h>
#include <lib/dnssd/TxtFields.h>
#include <lib/dnssd/Types.h>
#include <lib/support/CHIPMem.h>
#include <lib/support/CodeUtils.h>
#include <lib/support/Span.h>
namespace {
using namespace chip;
using namespace chip::Dnssd;
using namespace fuzztest;
void EnsureInitialized()
{
static const bool sInitialized = [] {
VerifyOrDie(Platform::MemoryInit() == CHIP_NO_ERROR);
return true;
}();
(void) sInitialized;
}
// Exact TXT key set from TxtFields.h (Internal::txtFieldInfo), plus a few
// near-miss / unknown keys so the kUnknown switch arm and the case-insensitive
// matcher are exercised too. Keeping these literal means every interesting
// branch of GetTxtFieldKey() is reachable from a seed rather than only by the
// mutator stumbling onto the right two bytes.
std::vector<std::string> KeySeeds()
{
return {
// ---- Common (operational) keys ----
"SII",
"SAI",
"SAT",
"T",
"ICD",
// ---- Commissionable / commissioner keys ----
"D",
"VP",
"CM",
"DT",
"DN",
"RI",
"PI",
"PH",
"CP",
// ---- Behind CHIP_DEVICE_CONFIG_ENABLE_JOINT_FABRIC, harmless otherwise ----
"JF",
// ---- Case variants (matcher is case-insensitive) ----
"d",
"cm",
"sii",
// ---- Unknown / near-miss / empty (kUnknown path) ----
"",
"X",
"DD",
"CRI",
"CRA",
"PAA",
};
}
// Realistic and boundary TXT values. Mix of in-range, boundary, and
// out-of-range forms so the numeric parsers (strtoul + range clamps) and the
// fixed-buffer string copies are both stressed.
std::vector<std::string> ValueSeeds()
{
return {
// ---- small / boundary integers ----
"",
"0",
"1",
"2",
"15",
// ---- leading-zero forms (parser rejects these -> default) ----
"01",
"00",
"007",
// ---- SII/SAI clamp boundary: kMaxRetryInterval == 3600000 ----
"3599999",
"3600000",
"3600001",
// ---- SAT / u16 boundary ----
"65534",
"65535",
"65536",
// ---- u8 boundary (CM) ----
"254",
"255",
"256",
// ---- VP "vendor+product" forms ----
"65521",
"65521+32769",
"840+15",
"+15",
"1+",
"+",
"0+0",
// ---- decimal overflow / way-too-long numbers ----
"99999999999",
"4294967295",
"4294967296",
"18446744073709551615",
"999999999999999999999999999999",
// ---- non-numeric / mixed ----
"abc",
"1a",
" 1",
"1 ",
"-1",
"+1",
"0x10",
"1.5",
// ---- rotating-device-id hex (RI): HexToBytes, capped at kMaxRotatingIdLen ----
"00112233445566778899AABBCCDDEEFF",
"0011223344",
"ZZ",
"0",
std::string(200, 'A'), // 100 hex bytes worth -> must clamp to 50
// ---- device name / pairing instruction oversize (fixed buffers) ----
std::string(32, 'n'), // exactly kMaxDeviceNameLen
std::string(33, 'n'), // one over
std::string(128, 'p'), // exactly kMaxPairingInstructionLen
std::string(129, 'p'), // one over
std::string(512, 'x'), // far over any buffer
// ---- embedded NUL / control bytes ----
std::string("a\0b", 3),
"\n",
"\t",
};
}
ByteSpan AsSpan(const std::string & s)
{
return ByteSpan(reinterpret_cast<const uint8_t *>(s.data()), s.size());
}
// Shared invariants on the CommonResolutionData portion of any parsed struct.
// These are the promises the field accessors make; violating one means the
// parser left a field outside its documented range.
void CheckCommonInvariants(const CommonResolutionData & data)
{
// SII / SAI: out-of-range input must collapse to nullopt, never a stored
// value above the documented maximum.
if (data.mrpRetryIntervalIdle.has_value())
{
ASSERT_LE(data.mrpRetryIntervalIdle->count(), kMaxRetryInterval.count());
}
if (data.mrpRetryIntervalActive.has_value())
{
ASSERT_LE(data.mrpRetryIntervalActive->count(), kMaxRetryInterval.count());
}
// SAT: 0 maps to nullopt, so a present value is always strictly positive.
if (data.mrpRetryActiveThreshold.has_value())
{
ASSERT_NE(data.mrpRetryActiveThreshold->count(), 0);
}
// hostName is a fixed buffer; the parser never touches it here, but assert
// it stayed NUL-terminated within bounds (cheap, catches stray writes).
ASSERT_LT(strnlen(data.hostName, sizeof(data.hostName)), sizeof(data.hostName));
}
// Property: parsing any (key, value) into CommonResolutionData never crashes
// and never violates the field-range invariants.
void FillCommonResolutionDataNoCorruption(const std::string & key, const std::string & value)
{
EnsureInitialized();
CommonResolutionData data;
FillNodeDataFromTxt(AsSpan(key), AsSpan(value), data);
CheckCommonInvariants(data);
}
FUZZ_TEST(TxtFieldsPW, FillCommonResolutionDataNoCorruption)
.WithDomains(Arbitrary<std::string>().WithSeeds(KeySeeds()).WithMaxSize(64),
Arbitrary<std::string>().WithSeeds(ValueSeeds()).WithMaxSize(1024));
// Property: parsing any (key, value) into CommissionNodeData never crashes and
// never violates the field-range invariants — including the rotating-device-id
// length bound and the fixed string-buffer bounds.
void FillCommissionNodeDataNoCorruption(const std::string & key, const std::string & value)
{
EnsureInitialized();
CommissionNodeData data;
FillNodeDataFromTxt(AsSpan(key), AsSpan(value), data);
// CommissionNodeData is-a CommonResolutionData; the common arms are reached
// via the default case of the commission overload.
CheckCommonInvariants(data);
// RI: HexToBytes must never report more bytes than the destination holds.
ASSERT_LE(data.rotatingIdLen, sizeof(data.rotatingId));
// DN / PI: Platform::CopyString must keep these NUL-terminated in-bounds.
ASSERT_LT(strnlen(data.deviceName, sizeof(data.deviceName)), sizeof(data.deviceName));
ASSERT_LT(strnlen(data.pairingInstruction, sizeof(data.pairingInstruction)), sizeof(data.pairingInstruction));
}
FUZZ_TEST(TxtFieldsPW, FillCommissionNodeDataNoCorruption)
.WithDomains(Arbitrary<std::string>().WithSeeds(KeySeeds()).WithMaxSize(64),
Arbitrary<std::string>().WithSeeds(ValueSeeds()).WithMaxSize(1024));
} // namespace