Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion packages/multicast_dns/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
## NEXT
## 0.3.2+8

* Fixes stack overflows ocurring during the parsing of domain names in MDNS messages.
* Updates minimum supported SDK version to Flutter 3.22/Dart 3.4.

## 0.3.2+7
Expand Down
112 changes: 46 additions & 66 deletions packages/multicast_dns/lib/src/packet.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

import 'dart:convert';
import 'dart:io';
import 'dart:math';
import 'dart:typed_data';

import 'constants.dart';
Expand Down Expand Up @@ -134,76 +135,55 @@ _FQDNReadResult _readFQDN(

final List<String> parts = <String>[];
final int prevOffset = offset;
while (true) {
// At least one byte is required.
checkLength(offset + 1);

// Check for compressed.
if (data[offset] & 0xc0 == 0xc0) {
// At least two bytes are required for a compressed FQDN.
checkLength(offset + 2);

// A compressed FQDN has a new offset in the lower 14 bits.
final _FQDNReadResult result = _readFQDN(
data, byteData, byteData.getUint16(offset) & ~0xc000, length);
parts.addAll(result.fqdnParts);
offset += 2;
break;
} else {
// A normal FQDN part has a length and a UTF-8 encoded name
// part. If the length is 0 this is the end of the FQDN.
final int partLength = data[offset];
offset++;
if (partLength > 0) {
checkLength(offset + partLength);
final Uint8List partBytes =
Uint8List.view(data.buffer, offset, partLength);
offset += partLength;
// According to the RFC, this is supposed to be utf-8 encoded, but
// we should continue decoding even if it isn't to avoid dropping the
// rest of the data, which might still be useful.
parts.add(utf8.decode(partBytes, allowMalformed: true));
} else {
final List<int> offsetsToVisit = <int>[offset];
int upperLimitOffset = offset;
int highestOffsetRead = offset;

while (offsetsToVisit.isNotEmpty) {
offset = offsetsToVisit.removeLast();

while (true) {
// At least one byte is required.
checkLength(offset + 1);
// Check for compressed.
if (data[offset] & 0xc0 == 0xc0) {
// At least two bytes are required for a compressed FQDN (see RFC1035 section 4.1.4).
checkLength(offset + 2);

// A compressed FQDN has a new offset in the lower 14 bits.
final int pointerDest = byteData.getUint16(offset) & ~0xc000;
// Pointers can only point to prior occurances of some name.
// This check also guards against pointers that form loops.
if (pointerDest >= upperLimitOffset) {
throw MDnsDecodeException(offset);
}
upperLimitOffset = pointerDest;
offsetsToVisit.add(pointerDest);
highestOffsetRead = max(highestOffsetRead, offset + 2);
break;
} else {
// A normal FQDN part has a length and a UTF-8 encoded name
// part. If the length is 0 this is the end of the FQDN.
final int partLength = data[offset];
offset++;
if (partLength > 0) {
checkLength(offset + partLength);
final Uint8List partBytes =
Uint8List.view(data.buffer, offset, partLength);
offset += partLength;
// According to the RFC, this is supposed to be utf-8 encoded, but
// we should continue decoding even if it isn't to avoid dropping the
// rest of the data, which might still be useful.
parts.add(utf8.decode(partBytes, allowMalformed: true));
highestOffsetRead = max(highestOffsetRead, offset);
} else {
highestOffsetRead = max(highestOffsetRead, offset);
break;
}
}
}
}
return _FQDNReadResult(parts, offset - prevOffset);
}

/// Decode an mDNS query packet.
///
/// If decoding fails (e.g. due to an invalid packet), `null` is returned.
///
/// See https://tools.ietf.org/html/rfc1035 for format.
ResourceRecordQuery? decodeMDnsQuery(List<int> packet) {
final int length = packet.length;
if (length < _kHeaderSize) {
return null;
}

final Uint8List data =
packet is Uint8List ? packet : Uint8List.fromList(packet);
final ByteData packetBytes = ByteData.view(data.buffer);

// Check whether it's a query.
final int flags = packetBytes.getUint16(_kFlagsOffset);
if (flags != 0) {
return null;
}
final int questionCount = packetBytes.getUint16(_kQdcountOffset);
if (questionCount == 0) {
return null;
}

final _FQDNReadResult fqdn =
_readFQDN(data, packetBytes, _kHeaderSize, data.length);

int offset = _kHeaderSize + fqdn.bytesRead;
final int type = packetBytes.getUint16(offset);
offset += 2;
final int queryType = packetBytes.getUint16(offset) & 0x8000;
return ResourceRecordQuery(type, fqdn.fqdn, queryType);
return _FQDNReadResult(parts, highestOffsetRead - prevOffset);
}

/// Decode an mDNS response packet.
Expand Down
2 changes: 1 addition & 1 deletion packages/multicast_dns/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ name: multicast_dns
description: Dart package for performing mDNS queries (e.g. Bonjour, Avahi).
repository: https://github.com/flutter/packages/tree/main/packages/multicast_dns
issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+multicast_dns%22
version: 0.3.2+7
version: 0.3.2+8

environment:
sdk: ^3.4.0
Expand Down
40 changes: 40 additions & 0 deletions packages/multicast_dns/test/decode_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,10 @@ void testBadPackages() {
}
}
});

test('Detects cyclic pointers and returns null', () {
expect(decodeMDnsResponse(cycle), isNull);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I verified that this is actually detecting a cycle using the debugger. We could refactor decodeMDnsResponse to return some sort of ADT that can indicate a reason for failure instead of always returning null. Then, tests could make sure that null is being returned by the expected reason. However, I want to make sure you are OK with this cycle-detection approach before I invest in that effort.

});
}

void testPTRRData() {
Expand Down Expand Up @@ -584,6 +588,42 @@ const List<int> package3 = <int>[
0x2c
];

/// Contains compressed domain names where a there is a cycle amongst the
/// offset pointers.
const List<int> cycle = <int>[
0x00,
0x00,
0x84,
0x00,
0x00,
0x00,
0x00,
0x01,
0x00,
0x00,
0x00,
0x00,
0x07, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, // "example"
0xC0, 0x16, // Pointer to "com"
0x03, 0x63, 0x6f, 0x6d, // "com"
0xC0, 0x0c, // Pointer to "example"
0x00,
0x00,
0x01,
0x80,
0x01,
0x00,
0x00,
0x00,
0x78,
0x00,
0x04,
0xc0,
0xa8,
0x01,
0xbf
];

const List<int> packagePtrResponse = <int>[
0x00,
0x00,
Expand Down
Loading