Skip to content

Commit

Permalink
[multicast_dns] Rewrite _readFQDN to avoid recursion (#8390)
Browse files Browse the repository at this point in the history
Speculatively fixes flutter/flutter#155433. Read [one of my later comments](flutter/flutter#155433 (comment)) on that thread for my current understanding of this issue. In summary, this PR rewrites `_readFQDN` to avoid recursion.

**For reviewers: For a simplified diff, see the first commit, [change](https://github.com/flutter/packages/commit/7afa1db54e07f024b2a4550d160b4d4a9f67f2c7).** This commit avoids formatting make sure the diff only highlights changes to logic rather than formatting.

<details>

<summary> Pre-launch Checklist </summary>

- x ] I signed the [CLA].

</details>
  • Loading branch information
andrewkolos authored Jan 27, 2025
1 parent 99f79d9 commit 15d1e92
Show file tree
Hide file tree
Showing 4 changed files with 89 additions and 68 deletions.
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);
});
}

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

0 comments on commit 15d1e92

Please sign in to comment.