Version 2.7.2
* Cherry-pick 5581307ae6ade7a6a093a9643993b159f270ac56 to stable
* Cherry-pick ca694e5ebf194e00644b77eeab31fd093640c5d8 to stable
* Cherry-pick 0c0e041a8d864804bdf7f09a5586a3de93a3259a to stable
* Cherry-pick 4af4245c45184ae41a74d4752f4ec819f387943f to stable
* Cherry-pick 6a5d73dfe7acc91249a37a4f80207da0da5f211b to stable
* Cherry-pick c4e526d7d8ba8352132192d3fbcd69ac2844c441 to stable
diff --git a/CHANGELOG.md b/CHANGELOG.md
index d316f79..5df7170 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,4 +1,17 @@
-## 2.7.1 - 2020-01-32
+## 2.7.2 - 2020-03-23
+
+This is a patch release that addresses a vulnerability dart:html
+[NodeValidator](https://api.dart.dev/stable/dart-html/NodeValidator-class.html)
+related to DOM clobbering of `previousSibling`. Thanks to **Vincenzo di Cicco**
+for finding and reporting this issue.
+
+This release also improves compatibility with ARMv8 processors
+(issue [40001][]) and dart:io stability (issue [40589][]).
+
+[40001]: https://github.com/dart-lang/sdk/issues/40001
+[40589]: https://github.com/dart-lang/sdk/issues/40589
+
+## 2.7.1 - 2020-01-23
This is a patch release that improves dart2js compile-time (issue [40217][]).
diff --git a/DEPS b/DEPS
index 8583d7a..6eacf04 100644
--- a/DEPS
+++ b/DEPS
@@ -276,8 +276,6 @@
Var("dart_git") + "dart_style.git" + "@" + Var("dart_style_tag"),
Var("dart_root") + "/third_party/pkg/dart2js_info":
Var("dart_git") + "dart2js_info.git" + "@" + Var("dart2js_info_tag"),
- Var("dart_root") + "/third_party/pkg/args":
- Var("dart_git") + "args.git" + "@" + Var("args_tag"),
Var("dart_root") + "/third_party/pkg/dartdoc":
Var("dart_git") + "dartdoc.git" + "@" + Var("dartdoc_tag"),
Var("dart_root") + "/third_party/pkg/ffi":
diff --git a/runtime/vm/cpu_arm.cc b/runtime/vm/cpu_arm.cc
index 4078722..2ee57f4 100644
--- a/runtime/vm/cpu_arm.cc
+++ b/runtime/vm/cpu_arm.cc
@@ -172,7 +172,8 @@
// Raspberry Pi, etc.
arm_version_ = ARMv6;
} else if (CpuInfo::FieldContains(kCpuInfoProcessor, "ARMv7") ||
- CpuInfo::FieldContains(kCpuInfoModel, "ARMv7")) {
+ CpuInfo::FieldContains(kCpuInfoModel, "ARMv7") ||
+ CpuInfo::FieldContains(kCpuInfoArchitecture, "7")) {
arm_version_ = ARMv7;
} else {
#if defined(DART_RUN_IN_QEMU_ARMv7)
diff --git a/sdk/lib/_internal/vm/bin/socket_patch.dart b/sdk/lib/_internal/vm/bin/socket_patch.dart
index 5771c76..b499efa 100644
--- a/sdk/lib/_internal/vm/bin/socket_patch.dart
+++ b/sdk/lib/_internal/vm/bin/socket_patch.dart
@@ -706,32 +706,52 @@
String get _serviceTypePath => throw new UnimplementedError();
String get _serviceTypeName => throw new UnimplementedError();
- Uint8List read(int len) {
- if (len != null && len <= 0) {
- throw new ArgumentError("Illegal length $len");
+ Uint8List read(int count) {
+ if (count != null && count <= 0) {
+ throw ArgumentError("Illegal length $count");
}
if (isClosing || isClosed) return null;
- len = min(available, len == null ? available : len);
- if (len == 0) return null;
- var result = nativeRead(len);
- if (result is OSError) {
- reportError(result, StackTrace.current, "Read failed");
- return null;
- }
- if (result != null) {
- available -= result.length;
- // TODO(ricow): Remove when we track internal and pipe uses.
- assert(resourceInfo != null || isPipe || isInternal || isInternalSignal);
- if (resourceInfo != null) {
- resourceInfo.totalRead += result.length;
+ var list;
+ if (count != null) {
+ list = nativeRead(count);
+ if (list is OSError) {
+ reportError(list, StackTrace.current, "Read failed");
+ return null;
+ }
+ } else {
+ // If count is null, read as many bytes as possible.
+ // Loop here to ensure bytes that arrived while this read was
+ // issued are also read.
+ BytesBuilder builder = BytesBuilder();
+ do {
+ assert(available > 0);
+ list = nativeRead(available);
+ if (list is OSError) {
+ reportError(list, StackTrace.current, "Read failed");
+ return null;
+ }
+ if (list == null) {
+ break;
+ }
+ builder.add(list);
+ available = nativeAvailable();
+ } while (available > 0);
+ if (builder.isEmpty) {
+ list = null;
+ } else {
+ list = builder.toBytes();
}
}
- // TODO(ricow): Remove when we track internal and pipe uses.
- assert(resourceInfo != null || isPipe || isInternal || isInternalSignal);
+ if (list != null) {
+ if (resourceInfo != null) {
+ resourceInfo.totalRead += list.length;
+ }
+ }
if (resourceInfo != null) {
resourceInfo.didRead();
}
- return result;
+ available = nativeAvailable();
+ return list;
}
Datagram receive() {
diff --git a/sdk/lib/html/dart2js/html_dart2js.dart b/sdk/lib/html/dart2js/html_dart2js.dart
index bdf8bac..87cab0f 100644
--- a/sdk/lib/html/dart2js/html_dart2js.dart
+++ b/sdk/lib/html/dart2js/html_dart2js.dart
@@ -13204,6 +13204,14 @@
if (!(element.attributes instanceof NamedNodeMap)) {
return true;
}
+ // If something has corrupted the traversal we want to detect
+ // these on not only the children (tested below) but on the node itself
+ // in case it was bypassed.
+ if (element["id"] == 'lastChild' || element["name"] == 'lastChild' ||
+ element["id"] == 'previousSibling' || element["name"] == 'previousSibling' ||
+ element["id"] == 'children' || element["name"] == 'children') {
+ return true;
+ }
var childNodes = element.childNodes;
if (element.lastChild &&
element.lastChild !== childNodes[childNodes.length -1]) {
@@ -13229,6 +13237,7 @@
// allowing us to check for clobbering that may show up in other accesses.
if (child["id"] == 'attributes' || child["name"] == 'attributes' ||
child["id"] == 'lastChild' || child["name"] == 'lastChild' ||
+ child["id"] == 'previousSibling' || child["name"] == 'previousSibling' ||
child["id"] == 'children' || child["name"] == 'children') {
return true;
}
@@ -39336,6 +39345,9 @@
*/
class _ValidatingTreeSanitizer implements NodeTreeSanitizer {
NodeValidator validator;
+
+ /// Did we modify the tree by removing anything.
+ bool modifiedTree = false;
_ValidatingTreeSanitizer(this.validator) {}
void sanitizeTree(Node node) {
@@ -39346,9 +39358,13 @@
while (null != child) {
var nextChild;
try {
- // Child may be removed during the walk, and we may not
- // even be able to get its previousNode.
+ // Child may be removed during the walk, and we may not even be able
+ // to get its previousNode. But it's also possible that previousNode
+ // (i.e. previousSibling) is being spoofed, so double-check it.
nextChild = child.previousNode;
+ if (nextChild != null && nextChild.nextNode != child) {
+ throw StateError("Corrupt HTML");
+ }
} catch (e) {
// Child appears bad, remove it. We want to check the rest of the
// children of node and, but we have no way of getting to the next
@@ -39362,7 +39378,12 @@
}
}
+ modifiedTree = false;
walk(node, null);
+ while (modifiedTree) {
+ modifiedTree = false;
+ walk(node, null);
+ }
}
/// Aggressively try to remove node.
@@ -39370,7 +39391,8 @@
// If we have the parent, it's presumably already passed more sanitization
// or is the fragment, so ask it to remove the child. And if that fails
// try to set the outer html.
- if (parent == null) {
+ modifiedTree = true;
+ if (parent == null || parent != node.parentNode) {
node.remove();
} else {
parent._removeChild(node);
diff --git a/sdk_nnbd/lib/_internal/vm/bin/socket_patch.dart b/sdk_nnbd/lib/_internal/vm/bin/socket_patch.dart
index 750aca8..689e88d 100644
--- a/sdk_nnbd/lib/_internal/vm/bin/socket_patch.dart
+++ b/sdk_nnbd/lib/_internal/vm/bin/socket_patch.dart
@@ -706,30 +706,55 @@
String get _serviceTypePath => throw new UnimplementedError();
String get _serviceTypeName => throw new UnimplementedError();
- Uint8List read(int len) {
- if (len != null && len <= 0) {
- throw new ArgumentError("Illegal length $len");
+ Uint8List? read(int? count) {
+ if (count != null && count <= 0) {
+ throw ArgumentError("Illegal length $count");
}
if (isClosing || isClosed) return null;
- len = min(available, len == null ? available : len);
- if (len == 0) return null;
- var result = nativeRead(len);
- if (result is OSError) {
- reportError(result, StackTrace.current, "Read failed");
- return null;
- }
- if (result != null) {
- available -= result.length;
- // TODO(ricow): Remove when we track internal and pipe uses.
- assert(resourceInfo != null || isPipe || isInternal || isInternalSignal);
- if (resourceInfo != null) {
- resourceInfo.totalRead += result.length;
+ var list;
+ if (count != null) {
+ list = nativeRead(count);
+ if (list is OSError) {
+ reportError(list, StackTrace.current, "Read failed");
+ return null;
+ }
+ } else {
+ // If count is null, read as many bytes as possible.
+ // Loop here to ensure bytes that arrived while this read was
+ // issued are also read.
+ BytesBuilder builder = BytesBuilder();
+ do {
+ assert(available > 0);
+ list = nativeRead(available);
+ if (list is OSError) {
+ reportError(list, StackTrace.current, "Read failed");
+ return null;
+ }
+ if (list == null) {
+ break;
+ }
+ builder.add(list);
+ available = nativeAvailable();
+ } while (available > 0);
+ if (builder.isEmpty) {
+ list = null;
+ } else {
+ list = builder.toBytes();
}
}
- // TODO(ricow): Remove when we track internal and pipe uses.
- assert(resourceInfo != null || isPipe || isInternal || isInternalSignal);
- if (resourceInfo != null) {
- resourceInfo.didRead();
+ final result = list as Uint8List?;
+ final resourceInformation = resourceInfo;
+ assert(resourceInformation != null ||
+ isPipe ||
+ isInternal ||
+ isInternalSignal);
+ if (result != null) {
+ if (resourceInformation != null) {
+ resourceInformation.totalRead += result.length;
+ }
+ }
+ if (resourceInformation != null) {
+ resourceInformation.didRead();
}
return result;
}
diff --git a/sdk_nnbd/lib/html/dart2js/html_dart2js.dart b/sdk_nnbd/lib/html/dart2js/html_dart2js.dart
index c7913ec..d1718a6 100644
--- a/sdk_nnbd/lib/html/dart2js/html_dart2js.dart
+++ b/sdk_nnbd/lib/html/dart2js/html_dart2js.dart
@@ -13203,6 +13203,14 @@
if (!(element.attributes instanceof NamedNodeMap)) {
return true;
}
+ // If something has corrupted the traversal we want to detect
+ // these on not only the children (tested below) but on the node itself
+ // in case it was bypassed.
+ if (element["id"] == 'lastChild' || element["name"] == 'lastChild' ||
+ element["id"] == 'previousSibling' || element["name"] == 'previousSibling' ||
+ element["id"] == 'children' || element["name"] == 'children') {
+ return true;
+ }
var childNodes = element.childNodes;
if (element.lastChild &&
element.lastChild !== childNodes[childNodes.length -1]) {
@@ -13228,6 +13236,7 @@
// allowing us to check for clobbering that may show up in other accesses.
if (child["id"] == 'attributes' || child["name"] == 'attributes' ||
child["id"] == 'lastChild' || child["name"] == 'lastChild' ||
+ child["id"] == 'previousSibling' || child["name"] == 'previousSibling' ||
child["id"] == 'children' || child["name"] == 'children') {
return true;
}
@@ -39335,6 +39344,9 @@
*/
class _ValidatingTreeSanitizer implements NodeTreeSanitizer {
NodeValidator validator;
+
+ /// Did we modify the tree by removing anything.
+ bool modifiedTree = false;
_ValidatingTreeSanitizer(this.validator) {}
void sanitizeTree(Node node) {
@@ -39345,9 +39357,13 @@
while (null != child) {
var nextChild;
try {
- // Child may be removed during the walk, and we may not
- // even be able to get its previousNode.
+ // Child may be removed during the walk, and we may not even be able
+ // to get its previousNode. But it's also possible that previousNode
+ // (i.e. previousSibling) is being spoofed, so double-check it.
nextChild = child.previousNode;
+ if (nextChild != null && nextChild.nextNode != child) {
+ throw StateError("Corrupt HTML");
+ }
} catch (e) {
// Child appears bad, remove it. We want to check the rest of the
// children of node and, but we have no way of getting to the next
@@ -39361,7 +39377,12 @@
}
}
+ modifiedTree = false;
walk(node, null);
+ while (modifiedTree) {
+ modifiedTree = false;
+ walk(node, null);
+ }
}
/// Aggressively try to remove node.
@@ -39369,7 +39390,8 @@
// If we have the parent, it's presumably already passed more sanitization
// or is the fragment, so ask it to remove the child. And if that fails
// try to set the outer html.
- if (parent == null) {
+ modifiedTree = true;
+ if (parent == null || parent != node.parentNode) {
node.remove();
} else {
parent._removeChild(node);
diff --git a/tests/lib_2/html/node_validator_important_if_you_suppress_make_the_bug_critical_test.dart b/tests/lib_2/html/node_validator_important_if_you_suppress_make_the_bug_critical_test.dart
index 194e43a..1b3ea60 100644
--- a/tests/lib_2/html/node_validator_important_if_you_suppress_make_the_bug_critical_test.dart
+++ b/tests/lib_2/html/node_validator_important_if_you_suppress_make_the_bug_critical_test.dart
@@ -428,6 +428,31 @@
"<form id='single_node_clobbering' onmouseover='alert(1)'><input name='attributes'>",
"");
+ testHtml('DOM clobbering of previousSibling',
+ validator,
+ 'https://example.com/<div><img/src="x"/onerror="alert();"/><form><input/name="previousSibling"></form></div>',
+ 'https://example.com/<div><img src="x"></div>'
+ );
+
+ // The lastChild and childNodes clobbering corrupts iterating the tree. They
+ // get removed, but in the meantime the evil node has been skipped. Note
+ // that we end up deleting the entire form because the clobbering makes its
+ // lastChild incorrect and we detect this, but only on the second time
+ // through.
+ testHtml('DOM clobbering of both childNodes and lastChild',
+ validator,
+ "abc<form><div><img src='x' onerror='alert(document.domain)'></div><div><input name='childNodes'><input id='lastChild' name='childNodes'></div></form>",
+ 'abc'
+ );
+
+ test('DOM clobbering of previousSibling via append', () {
+ var div = Element.div();
+ var bad =
+ 'https://example.com/<div><img/src="x"/onerror="alert();"/><form><input/name="previousSibling"></form></div>';
+ div.innerHtml = bad;
+ expect(div.innerHtml, 'https://example.com/<div><img src="x"></div>');
+ });
+
testHtml(
'DOM clobbering of attributes with multiple nodes',
validator,
diff --git a/tests/standalone/io/socket_hang_test.dart b/tests/standalone/io/socket_hang_test.dart
new file mode 100644
index 0000000..2231844
--- /dev/null
+++ b/tests/standalone/io/socket_hang_test.dart
@@ -0,0 +1,24 @@
+// Copyright (c) 2020, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'dart:convert';
+import 'dart:io' as io;
+
+void main(List<String> args) async {
+ if (args.length != 0) {
+ for (int i = 0; i < 100000; i++) {
+ print('line $i');
+ }
+ print('done');
+ return;
+ } else {
+ // Create child process and keeps writing into stdout.
+ final p = await io.Process.start(
+ io.Platform.executable, [io.Platform.script.toFilePath(), 'child']);
+ p.stdout.transform(utf8.decoder).listen((x) => print('stdout: $x'));
+ p.stderr.transform(utf8.decoder).listen((x) => print('stderr: $x'));
+ final exitCode = await p.exitCode;
+ print('process exited with ${exitCode}');
+ }
+}
diff --git a/tests/standalone_2/io/socket_hang_test.dart b/tests/standalone_2/io/socket_hang_test.dart
new file mode 100644
index 0000000..2231844
--- /dev/null
+++ b/tests/standalone_2/io/socket_hang_test.dart
@@ -0,0 +1,24 @@
+// Copyright (c) 2020, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'dart:convert';
+import 'dart:io' as io;
+
+void main(List<String> args) async {
+ if (args.length != 0) {
+ for (int i = 0; i < 100000; i++) {
+ print('line $i');
+ }
+ print('done');
+ return;
+ } else {
+ // Create child process and keeps writing into stdout.
+ final p = await io.Process.start(
+ io.Platform.executable, [io.Platform.script.toFilePath(), 'child']);
+ p.stdout.transform(utf8.decoder).listen((x) => print('stdout: $x'));
+ p.stderr.transform(utf8.decoder).listen((x) => print('stderr: $x'));
+ final exitCode = await p.exitCode;
+ print('process exited with ${exitCode}');
+ }
+}
diff --git a/tools/VERSION b/tools/VERSION
index 42b0140..9ccd548 100644
--- a/tools/VERSION
+++ b/tools/VERSION
@@ -32,7 +32,7 @@
CHANNEL stable
MAJOR 2
MINOR 7
-PATCH 1
+PATCH 2
PRERELEASE 0
PRERELEASE_PATCH 0
ABI_VERSION 24
diff --git a/tools/dom/src/Validators.dart b/tools/dom/src/Validators.dart
index ac0663d..a779f87 100644
--- a/tools/dom/src/Validators.dart
+++ b/tools/dom/src/Validators.dart
@@ -156,6 +156,9 @@
*/
class _ValidatingTreeSanitizer implements NodeTreeSanitizer {
NodeValidator validator;
+
+ /// Did we modify the tree by removing anything.
+ bool modifiedTree = false;
_ValidatingTreeSanitizer(this.validator) {}
void sanitizeTree(Node node) {
@@ -166,9 +169,13 @@
while (null != child) {
var nextChild;
try {
- // Child may be removed during the walk, and we may not
- // even be able to get its previousNode.
+ // Child may be removed during the walk, and we may not even be able
+ // to get its previousNode. But it's also possible that previousNode
+ // (i.e. previousSibling) is being spoofed, so double-check it.
nextChild = child.previousNode;
+ if (nextChild != null && nextChild.nextNode != child) {
+ throw StateError("Corrupt HTML");
+ }
} catch (e) {
// Child appears bad, remove it. We want to check the rest of the
// children of node and, but we have no way of getting to the next
@@ -182,7 +189,12 @@
}
}
+ modifiedTree = false;
walk(node, null);
+ while (modifiedTree) {
+ modifiedTree = false;
+ walk(node, null);
+ }
}
/// Aggressively try to remove node.
@@ -190,7 +202,8 @@
// If we have the parent, it's presumably already passed more sanitization
// or is the fragment, so ask it to remove the child. And if that fails
// try to set the outer html.
- if (parent == null) {
+ modifiedTree = true;
+ if (parent == null || parent != node.parentNode) {
node.remove();
} else {
parent._removeChild(node);
diff --git a/tools/dom/templates/html/impl/impl_Element.darttemplate b/tools/dom/templates/html/impl/impl_Element.darttemplate
index 3fd0d4e..5a24280 100644
--- a/tools/dom/templates/html/impl/impl_Element.darttemplate
+++ b/tools/dom/templates/html/impl/impl_Element.darttemplate
@@ -1442,6 +1442,14 @@
if (!(element.attributes instanceof NamedNodeMap)) {
return true;
}
+ // If something has corrupted the traversal we want to detect
+ // these on not only the children (tested below) but on the node itself
+ // in case it was bypassed.
+ if (element["id"] == 'lastChild' || element["name"] == 'lastChild' ||
+ element["id"] == 'previousSibling' || element["name"] == 'previousSibling' ||
+ element["id"] == 'children' || element["name"] == 'children') {
+ return true;
+ }
var childNodes = element.childNodes;
if (element.lastChild &&
element.lastChild !== childNodes[childNodes.length -1]) {
@@ -1467,6 +1475,7 @@
// allowing us to check for clobbering that may show up in other accesses.
if (child["id"] == 'attributes' || child["name"] == 'attributes' ||
child["id"] == 'lastChild' || child["name"] == 'lastChild' ||
+ child["id"] == 'previousSibling' || child["name"] == 'previousSibling' ||
child["id"] == 'children' || child["name"] == 'children') {
return true;
}