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