[mdns] reject empty PTR target label on receive (#13183)

`PtrRecord::ReadPtrName()` reads a PTR target's first label with
`Name::ReadLabel()`, which performs no emptiness check. A response whose
first label is a single NUL byte (wire `01 00`) is stored as an empty
C-string and cached by the browse cache as a service instance. When the
cache later builds a known-answer question, it calls
`Name::AppendLabel("")`, which returns `kErrorInvalidArgs`; the
surrounding `SuccessOrAssert()` turns that into an abort. A single
unauthenticated link-local mDNS response thus crashes any node with an
active browser.

Reject an empty first label in `ReadPtrName()` so the record is dropped
on receive and never cached. This matches the `Name::ValidateLabel`
checks already applied on the registration and resolver paths, and makes
the "ReadPtrName() validates that PTR record is well-formed" comment at
the call site accurate.

Add a regression test that delivers a PTR response with a single
NUL-byte instance label and verifies no result is reported and the
browser keeps querying without the malformed entry.
This commit is contained in:
mohammadmseet-hue
2026-06-01 23:19:16 +03:00
committed by GitHub
parent 32b96a0d98
commit 675162556b
2 changed files with 115 additions and 0 deletions
+4
View File
@@ -1592,6 +1592,10 @@ Error PtrRecord::ReadPtrName(const Message &aMessage,
aOffset = startOffset + sizeof(PtrRecord);
SuccessOrExit(error = Name::ReadLabel(aMessage, aOffset, aLabelBuffer, aLabelBufferSize));
// The first label of a PTR target (the service-instance or host label)
// must be non-empty.
VerifyOrExit(aLabelBuffer[0] != kNullChar, error = kErrorParse);
if (aNameBuffer != nullptr)
{
SuccessOrExit(error = Name::ReadName(aMessage, aOffset, aNameBuffer, aNameBufferSize));
+111
View File
@@ -1331,6 +1331,53 @@ static void SendPtrResponse(const char *aName, const char *aPtrName, uint32_t aT
otPlatMdnsHandleReceive(sInstance, message, /* aIsUnicast */ false, &senderAddrInfo);
}
static void SendPtrResponseWithEmptyInstanceLabel(const char *aName, uint32_t aTtl)
{
// Sends a PTR response whose target name starts with a single
// NUL-byte (empty) label, i.e. the wire label `01 00` followed by
// `aName`. The RDLENGTH is computed from the appended target bytes.
static const uint8_t kEmptyLabel[] = {1, 0};
Message *message;
Header header;
PtrRecord ptr;
Core::AddressInfo senderAddrInfo;
uint16_t ptrOffset;
uint16_t rdataOffset;
message = sInstance->Get<MessagePool>().Allocate(Message::kTypeOther);
VerifyOrQuit(message != nullptr);
header.Clear();
header.SetType(Header::kTypeResponse);
header.SetAnswerCount(1);
SuccessOrQuit(message->Append(header));
SuccessOrQuit(Name::AppendName(aName, *message));
ptr.Init();
ptr.SetTtl(aTtl);
ptr.SetLength(0);
ptrOffset = message->GetLength();
SuccessOrQuit(message->Append(ptr));
rdataOffset = message->GetLength();
SuccessOrQuit(message->AppendBytes(kEmptyLabel, sizeof(kEmptyLabel)));
SuccessOrQuit(Name::AppendName(aName, *message));
ptr.SetLength(message->GetLength() - rdataOffset);
message->WriteBytes(ptrOffset, &ptr, sizeof(ptr));
SuccessOrQuit(AsCoreType(&senderAddrInfo.mAddress).FromString(kDeviceIp6Address));
senderAddrInfo.mPort = kMdnsPort;
senderAddrInfo.mInfraIfIndex = 0;
Log("Sending PTR response for %s with empty instance label, ttl:%lu", aName, ToUlong(aTtl));
otPlatMdnsHandleReceive(sInstance, message, /* aIsUnicast */ false, &senderAddrInfo);
}
static void SendSrvResponse(const char *aServiceName,
const char *aHostName,
uint16_t aPort,
@@ -6223,6 +6270,69 @@ void TestBrowser(void)
testFreeInstance(sInstance);
}
void TestBrowserMalformedPtrName(void)
{
Core *mdns = InitTest();
Core::Browser browser;
const DnsMessage *dnsMsg;
uint16_t heapAllocations;
Log("-------------------------------------------------------------------------------------------");
Log("TestBrowserMalformedPtrName");
AdvanceTime(1);
heapAllocations = sHeapAllocatedPtrs.GetLength();
SuccessOrQuit(mdns->SetEnabled(true, kInfraIfIndex));
ClearAllBytes(browser);
browser.mServiceType = "_srv._udp";
browser.mSubTypeLabel = nullptr;
browser.mInfraIfIndex = kInfraIfIndex;
browser.mCallback = HandleBrowseResult;
sDnsMessages.Clear();
sBrowseCallbacks.Clear();
SuccessOrQuit(mdns->StartBrowser(browser));
Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -");
Log("Send a PTR response whose service-instance label is a single NUL byte");
SendPtrResponseWithEmptyInstanceLabel("_srv._udp.local.", 120);
AdvanceTime(1);
Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -");
Log("Validate that the malformed record is dropped and no result is reported");
VerifyOrQuit(sBrowseCallbacks.IsEmpty());
Log("- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -");
Log("Validate that the browser keeps querying with no known-answer for the dropped record");
for (uint8_t queryCount = 0; queryCount < kNumInitialQueries; queryCount++)
{
sDnsMessages.Clear();
AdvanceTime(DetermineQueryWaitTime(queryCount));
VerifyOrQuit(!sDnsMessages.IsEmpty());
dnsMsg = sDnsMessages.GetHead();
dnsMsg->ValidateHeader(kMulticastQuery, /* Q */ 1, /* Ans */ 0, /* Auth */ 0, /* Addnl */ 0);
dnsMsg->ValidateAsQueryFor(browser);
VerifyOrQuit(dnsMsg->GetNext() == nullptr);
}
VerifyOrQuit(sBrowseCallbacks.IsEmpty());
SuccessOrQuit(mdns->SetEnabled(false, kInfraIfIndex));
VerifyOrQuit(sHeapAllocatedPtrs.GetLength() <= heapAllocations);
Log("End of test");
testFreeInstance(sInstance);
}
void TestSrvResolver(void)
{
Core *mdns = InitTest();
@@ -8923,6 +9033,7 @@ int main(void)
ot::Dns::Multicast::TestServiceConflict();
ot::Dns::Multicast::TestBrowser();
ot::Dns::Multicast::TestBrowserMalformedPtrName();
ot::Dns::Multicast::TestSrvResolver();
ot::Dns::Multicast::TestTxtResolver();
ot::Dns::Multicast::TestIp6AddrResolver();