blob: 2113716e2c86ed8c9fb948c36bff845854676cf0 [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 FuzzTest harness for ServiceNaming: the DNS-SD name builders
* (MakeInstanceName, MakeHostName, MakeServiceSubtype, MakeServiceTypeName)
* and the matching parser, ExtractIdFromInstanceName.
*
* ExtractIdFromInstanceName runs on the instance label of a received
* record, so it is driven here from a string domain seeded with valid
* <compressedFabricId>-<nodeId> hex pairs alongside malformed forms (wrong
* separator count, non-hex digits, over- and under-length halves, empty
* halves). The builders are driven with the buffer length as a domain of its
* own, so the truncation paths are exercised rather than only the
* comfortable case.
*
* Oracles beyond no-crash under ASan/UBSan:
* - every builder either reports failure, or leaves a NUL strictly inside
* the destination it was given;
* - a name that ExtractIdFromInstanceName accepts round-trips: rebuilding
* it with MakeInstanceName and extracting again yields the same PeerId.
*/
#include <array>
#include <cstdint>
#include <cstring>
#include <string>
#include <vector>
#include <fuzztest/fuzztest_core.h>
#include <gtest/gtest.h>
#include <lib/core/PeerId.h>
#include <lib/dnssd/Constants.h>
#include <lib/dnssd/ServiceNaming.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;
}
// Buffers are deliberately larger than the documented maxima so an overrun would
// land in the guard area rather than in an adjacent object.
constexpr size_t kNameBufferSize = 128;
// Instance labels. The accepted form is <compressedFabricId>-<nodeId>, both as
// 16 hex digits; the rest are the near misses a name off the wire can take.
std::vector<std::string> InstanceNameSeeds()
{
return {
// Accepted forms.
"1234567898765432-ABCDEFEDCBAABCDE",
"0000000000000000-0000000000000000",
"FFFFFFFFFFFFFFFF-FFFFFFFFFFFFFFFF",
"1234567898765432-abcdefedcbaabcde", // lower-case hex
// Separator problems.
"1234567898765432ABCDEFEDCBAABCDE", // missing separator
"1234567898765432-ABCDEFEDCBAABCDE-1234567898765432",
"-",
"-ABCDEFEDCBAABCDE",
"1234567898765432-",
// Length problems on one half.
"123456789876543-ABCDEFEDCBAABCDE", // short first
"12345678987654321-ABCDEFEDCBAABCDE", // long first
"1234567898765432-ABCDEFEDCBAABCD", // short second
"1234567898765432-ABCDEFEDCBAABCDEF", // long second
// Not hex.
"123456789876543G-ABCDEFEDCBAABCDE",
"0x12345678987654-ABCDEFEDCBAABCDE",
"1234567898765432-ZZZZZZZZZZZZZZZZ",
"1234567898765432- BCDEFEDCBAABCDE",
// Degenerate.
"",
"0",
std::string(33, 'A'),
std::string(64, 'A'),
// Embedded NUL, so the parser's reliance on termination is exercised.
std::string("1234567898765432-ABCDEFEDCB\0ABCDE", 32),
};
}
// MAC / EUI64 payloads for MakeHostName: the two documented lengths plus the
// boundaries either side.
std::vector<std::vector<uint8_t>> MacSeeds()
{
return {
{ 0xB7, 0x5A, 0xFB, 0x45, 0x8E, 0xCD }, // 6-byte MAC
{ 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77 }, // 8-byte EUI64
{}, // empty
{ 0xAA }, // 1
{ 0x00, 0x11, 0x22, 0x33, 0x44 }, // 5
{ 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66 }, // 7
{ 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88 }, // 9
std::vector<uint8_t>(32, 0xFF), // far over
};
}
// A builder either fails, or leaves a NUL strictly inside the buffer it was
// handed. A success that filled the buffer without terminating would make every
// later strlen on the name read past the end.
void CheckBuilderResult(CHIP_ERROR err, const char * buffer, size_t bufferLen)
{
if (err != CHIP_NO_ERROR)
{
return;
}
ASSERT_GT(bufferLen, 0u);
ASSERT_LT(strnlen(buffer, bufferLen), bufferLen);
}
// Property: the instance-name parser survives any label and, when it accepts
// one, the id it produced rebuilds into a label that parses back identically.
void ExtractInstanceNameNoCorruption(const std::string & instanceName)
{
EnsureInitialized();
PeerId peerId;
if (ExtractIdFromInstanceName(instanceName.c_str(), &peerId) != CHIP_NO_ERROR)
{
return;
}
char rebuilt[kNameBufferSize] = {};
ASSERT_EQ(MakeInstanceName(rebuilt, sizeof(rebuilt), peerId), CHIP_NO_ERROR);
ASSERT_LT(strnlen(rebuilt, sizeof(rebuilt)), sizeof(rebuilt));
PeerId roundTripped;
ASSERT_EQ(ExtractIdFromInstanceName(rebuilt, &roundTripped), CHIP_NO_ERROR);
ASSERT_EQ(roundTripped.GetCompressedFabricId(), peerId.GetCompressedFabricId());
ASSERT_EQ(roundTripped.GetNodeId(), peerId.GetNodeId());
}
FUZZ_TEST(ServiceNamingPW, ExtractInstanceNameNoCorruption)
.WithDomains(Arbitrary<std::string>().WithSeeds(InstanceNameSeeds()).WithMaxSize(128));
// Property: every name builder tolerates any destination length, including
// lengths too small for the name it was asked to write.
void BuildNamesNoCorruption(uint64_t compressedFabricId, uint64_t nodeId, const std::vector<uint8_t> & mac, uint8_t filterTypeSel,
uint64_t filterCode, const std::string & filterInstanceName, uint8_t discoveryTypeSel,
uint8_t bufferLenSel)
{
EnsureInitialized();
// One byte upwards. That is small enough to drive every truncation path while
// staying within the lengths the builders are documented for; a zero-length
// destination is not a length any caller in the tree passes.
const size_t bufferLen = 1 + (bufferLenSel % kNameBufferSize);
std::array<char, kNameBufferSize> buffer;
{
buffer.fill('\xAB');
const PeerId peerId = PeerId().SetCompressedFabricId(compressedFabricId).SetNodeId(nodeId);
CheckBuilderResult(MakeInstanceName(buffer.data(), bufferLen, peerId), buffer.data(), bufferLen);
}
{
buffer.fill('\xAB');
const CHIP_ERROR err = MakeHostName(buffer.data(), bufferLen, ByteSpan(mac.data(), mac.size()));
// MakeHostName writes one hex pair per input byte, so an empty span
// leaves the destination exactly as it found it. Empty spans are still
// passed through to cover that path, but the termination check only
// applies where the builder was asked to write something.
if (!mac.empty())
{
CheckBuilderResult(err, buffer.data(), bufferLen);
}
}
// kCompressedFabricId is the last enumerator, so the count is one past it.
constexpr uint8_t kFilterTypeCount = static_cast<uint8_t>(DiscoveryFilterType::kCompressedFabricId) + 1;
constexpr uint8_t kDiscoveryTypeCount = static_cast<uint8_t>(DiscoveryType::kCommissionerNode) + 1;
DiscoveryFilter filter;
filter.type = static_cast<DiscoveryFilterType>(filterTypeSel % kFilterTypeCount);
filter.code = filterCode;
// kInstanceName is the one filter that reads the string rather than the code.
filter.instanceName = (filter.type == DiscoveryFilterType::kInstanceName) ? filterInstanceName.c_str() : nullptr;
{
buffer.fill('\xAB');
CheckBuilderResult(MakeServiceSubtype(buffer.data(), bufferLen, filter), buffer.data(), bufferLen);
}
{
buffer.fill('\xAB');
const DiscoveryType type = static_cast<DiscoveryType>(discoveryTypeSel % kDiscoveryTypeCount);
CheckBuilderResult(MakeServiceTypeName(buffer.data(), bufferLen, filter, type), buffer.data(), bufferLen);
}
}
FUZZ_TEST(ServiceNamingPW, BuildNamesNoCorruption)
.WithDomains(/* compressedFabricId */ Arbitrary<uint64_t>(),
/* nodeId */ Arbitrary<uint64_t>(),
/* mac */ Arbitrary<std::vector<uint8_t>>().WithSeeds(MacSeeds()).WithMaxSize(64),
/* filterTypeSel */ Arbitrary<uint8_t>(),
/* filterCode */ Arbitrary<uint64_t>(),
/* filterInstanceName */ Arbitrary<std::string>().WithSeeds(InstanceNameSeeds()).WithMaxSize(128),
/* discoveryTypeSel */ Arbitrary<uint8_t>(),
/* bufferLenSel */ Arbitrary<uint8_t>());
} // namespace