Fix two crashes when reading invalid pax headers
diff --git a/CHANGELOG.md b/CHANGELOG.md index 889d312..402f3d5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md
@@ -1,3 +1,9 @@ +## 1.0.3 + +- Fix the reader throwing a `FormatException` (instead of a `TarException`) + when reading tar files with invalid UTF bytes in their PAX headers. +- Fix a range error for invalid zero-length PAX entries. + ## 1.0.2 - Fix a few typos in documentation comments.
diff --git a/lib/src/reader.dart b/lib/src/reader.dart index 3148a65..d7cc33c 100644 --- a/lib/src/reader.dart +++ b/lib/src/reader.dart
@@ -730,6 +730,18 @@ Never error() => throw TarException.header('Invalid PAX record'); + String decodeString( + List<int> data, int start, int end, bool allowMalformed) { + assert(start < data.length && end <= data.length && start < end); + final decoder = allowMalformed ? unsafeUtf8Decoder : utf8.decoder; + + try { + return decoder.convert(data, start, end); + } on FormatException { + error(); + } + } + while (offset < data.length) { // At the start of an entry, expect its length which is terminated by a // space char. @@ -763,13 +775,14 @@ error(); } - // Read the key + // Read the key, and ensure there is enough space for the value and the + // trailing newline. final nextEquals = data.indexOf($equal, offset); - if (nextEquals == -1 || nextEquals >= endOfEntry) { + if (nextEquals == -1 || nextEquals >= endOfEntry - 1) { error(); } - final key = utf8.decoder.convert(data, offset, nextEquals); + final key = decodeString(data, offset, nextEquals, false); // Skip over the equals sign offset = nextEquals + 1; @@ -783,7 +796,7 @@ // If we're seeing weird PAX Version 0.0 sparse keys, expect alternating // GNU.sparse.offset and GNU.sparse.numbytes headers. if (key == paxGNUSparseNumBytes || key == paxGNUSparseOffset) { - final value = utf8.decoder.convert(data, offset, endOfValue); + final value = decodeString(data, offset, endOfValue, false); if (!_isValidPaxRecord(key, value) || (sparseMap.length.isEven && key != paxGNUSparseOffset) || @@ -796,7 +809,7 @@ } else if (!ignoreUnknown || supportedPaxHeaders.contains(key)) { // Ignore unrecognized headers to avoid unbounded growth of the global // header map. - final value = unsafeUtf8Decoder.convert(data, offset, endOfValue); + final value = decodeString(data, offset, endOfValue, true); if (!_isValidPaxRecord(key, value)) { error();
diff --git a/pubspec.yaml b/pubspec.yaml index bed57e7..be54f62 100644 --- a/pubspec.yaml +++ b/pubspec.yaml
@@ -1,6 +1,6 @@ name: tar description: Memory-efficient, streaming implementation of the tar file format -version: 1.0.2 +version: 1.0.3 repository: https://github.com/simolus3/tar/ environment:
diff --git a/reference/bad/invalid_pax_header.tar b/reference/bad/invalid_pax_header.tar new file mode 100644 index 0000000..6a5c581 --- /dev/null +++ b/reference/bad/invalid_pax_header.tar Binary files differ
diff --git a/reference/bad/invalid_pax_len.tar b/reference/bad/invalid_pax_len.tar new file mode 100644 index 0000000..ba49b86 --- /dev/null +++ b/reference/bad/invalid_pax_len.tar Binary files differ
diff --git a/test/reader_test.dart b/test/reader_test.dart index d1abde5..f0a2147 100644 --- a/test/reader_test.dart +++ b/test/reader_test.dart
@@ -972,6 +972,18 @@ }); }); + test('throws for invalid utf8 in pax key', () async { + final reader = + TarReader(fs.file('reference/bad/invalid_pax_header.tar').openRead()); + expect(reader.moveNext(), throwsA(isA<TarException>())); + }); + + test('throws for zero-length pax data', () async { + final reader = + TarReader(fs.file('reference/bad/invalid_pax_len.tar').openRead()); + expect(reader.moveNext(), throwsA(isA<TarException>())); + }); + group('PAX headers', () { test('locals overwrite globals', () { final header = PaxHeaders()
diff --git a/tool/fuzz.dart b/tool/fuzz.dart new file mode 100644 index 0000000..0909385 --- /dev/null +++ b/tool/fuzz.dart
@@ -0,0 +1,30 @@ +import 'dart:io'; + +import 'package:tar/tar.dart'; + +const verbose = bool.fromEnvironment('verbose'); + +/// Reads tar files from arguments and expects not to crash. +/// +/// When something goes wrong, the name of the problematic tar file is printed. +/// By running with `-Dverbose=true`, a stack trace is printed as well. +void main(List<String> files) async { + for (final file in files) { + try { + await TarReader.forEach(File(file).openRead(), (entry) {}); + } on TarException { + // These are fine + } on Object catch (e, s) { + // Other exceptions indicate a bug in pkg:tar + if (verbose) { + print(e); + print(s); + } else { + print( + 'failed for $file - run `dart -Dverbose=true tool/fuzz.dart $file`'); + } + + exit(128); + } + } +}
diff --git a/tool/run_fuzz.sh b/tool/run_fuzz.sh new file mode 100755 index 0000000..0ad165f --- /dev/null +++ b/tool/run_fuzz.sh
@@ -0,0 +1,9 @@ +rm -r /tmp/fuzz +mkdir /tmp/fuzz +dart compile exe tool/fuzz.dart -o /tmp/fuzz/fuzz.exe + +while true; do + radamsa -o /tmp/fuzz/gen-%n -n 100 reference/**/*.tar + /tmp/fuzz/fuzz.exe /tmp/fuzz/gen-* + test $? -gt 127 && break +done