blob: aa96d5c2c2342be7d5f37d39aca81121bc16dcbd [file] [log] [blame]
// Copyright 2022 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package org.chromium.android_webview.test.services;
import static org.chromium.android_webview.test.OnlyRunIn.ProcessMode.SINGLE_PROCESS;
import android.content.Context;
import android.os.Build;
import androidx.annotation.RequiresApi;
import androidx.javascriptengine.EvaluationFailedException;
import androidx.javascriptengine.EvaluationResultSizeLimitExceededException;
import androidx.javascriptengine.IsolateStartupParameters;
import androidx.javascriptengine.IsolateTerminatedException;
import androidx.javascriptengine.JavaScriptConsoleCallback;
import androidx.javascriptengine.JavaScriptIsolate;
import androidx.javascriptengine.JavaScriptSandbox;
import androidx.javascriptengine.MemoryLimitExceededException;
import androidx.javascriptengine.SandboxDeadException;
import androidx.test.filters.LargeTest;
import androidx.test.filters.MediumTest;
import com.google.common.util.concurrent.ListenableFuture;
import org.junit.Assert;
import org.junit.Assume;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.chromium.android_webview.test.AwJUnit4ClassRunner;
import org.chromium.android_webview.test.OnlyRunIn;
import org.chromium.base.ContextUtils;
import org.chromium.base.test.util.DisabledTest;
import org.chromium.base.test.util.MinAndroidSdkLevel;
import java.nio.charset.StandardCharsets;
import java.util.Vector;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
/**
* Instrumentation test for JavaScriptSandbox.
*/
@RunWith(AwJUnit4ClassRunner.class)
@MinAndroidSdkLevel(Build.VERSION_CODES.O)
@RequiresApi(Build.VERSION_CODES.O)
@OnlyRunIn(SINGLE_PROCESS)
public class JsSandboxServiceTest {
// This value is somewhat arbitrary. It might need bumping if V8 snapshots become significantly
// larger in future. However, we don't want it too large as that will make the tests slower and
// require more memory.
private static final long REASONABLE_HEAP_SIZE = 100 * 1024 * 1024;
private static final int LARGE_NAMED_DATA_SIZE = 2 * 1024 * 1024;
// ASCII, embedded null, Latin-1 supplement, code points above 0xff, and surrogate pairs.
private static final String UNICODE_TEST_STRING =
"Hello \u0000 Hell\u00f3 \u4f60\u597d \ud83d\udc4b";
private static final String JS_UNICODE_TEST_STRING =
"'Hello \u0000 Hell\u00f3 \u4f60\u597d \ud83d\udc4b'";
// Prefer this unless you are deliberately testing script input. Sending the script in pure
// ASCII reduces the probability that there may be both input and output bugs which cancel each
// other out.
private static final String ASCII_ESCAPED_JS_UNICODE_TEST_STRING =
"'Hello \\u0000 Hell\\u00f3 \\u4f60\\u597d \\ud83d\\udc4b'";
private static void assertStringEndsWithValidCodePoint(String string) {
Assert.assertNotNull(string);
if (string.length() == 0) {
return;
}
char lastChar = string.charAt(string.length() - 1);
Assert.assertFalse(Character.isHighSurrogate(lastChar));
// Reject replacement character
Assert.assertNotEquals(0xfffd, lastChar);
}
@Test
@MediumTest
public void testSimpleJsEvaluation() throws Throwable {
final String code = "\"PASS\"";
final String expected = "PASS";
Context context = ContextUtils.getApplicationContext();
ListenableFuture<JavaScriptSandbox> jsSandboxFuture =
JavaScriptSandbox.createConnectedInstanceForTestingAsync(context);
try (JavaScriptSandbox jsSandbox = jsSandboxFuture.get(5, TimeUnit.SECONDS);
JavaScriptIsolate jsIsolate = jsSandbox.createIsolate()) {
ListenableFuture<String> resultFuture = jsIsolate.evaluateJavaScriptAsync(code);
String result = resultFuture.get(5, TimeUnit.SECONDS);
Assert.assertEquals(expected, result);
}
}
@Test
@MediumTest
public void testClosingOneIsolate() throws Throwable {
final String code = "'PASS'";
final String expected = "PASS";
Context context = ContextUtils.getApplicationContext();
ListenableFuture<JavaScriptSandbox> jsSandboxFuture =
JavaScriptSandbox.createConnectedInstanceForTestingAsync(context);
try (JavaScriptSandbox jsSandbox = jsSandboxFuture.get(5, TimeUnit.SECONDS);
JavaScriptIsolate jsIsolate1 = jsSandbox.createIsolate()) {
JavaScriptIsolate jsIsolate2 = jsSandbox.createIsolate();
jsIsolate2.close();
ListenableFuture<String> resultFuture = jsIsolate1.evaluateJavaScriptAsync(code);
String result = resultFuture.get(5, TimeUnit.SECONDS);
Assert.assertEquals(expected, result);
}
}
@Test
@MediumTest
public void testEvaluationInTwoIsolates() throws Throwable {
final String code1 = "this.x = 'PASS';\n";
final String expected1 = "PASS";
final String code2 = "this.x = 'SUPER_PASS';\n";
final String expected2 = "SUPER_PASS";
Context context = ContextUtils.getApplicationContext();
ListenableFuture<JavaScriptSandbox> jsSandboxFuture =
JavaScriptSandbox.createConnectedInstanceForTestingAsync(context);
try (JavaScriptSandbox jsSandbox = jsSandboxFuture.get(5, TimeUnit.SECONDS);
JavaScriptIsolate jsIsolate1 = jsSandbox.createIsolate();
JavaScriptIsolate jsIsolate2 = jsSandbox.createIsolate()) {
ListenableFuture<String> resultFuture1 = jsIsolate1.evaluateJavaScriptAsync(code1);
String result1 = resultFuture1.get(5, TimeUnit.SECONDS);
ListenableFuture<String> resultFuture2 = jsIsolate2.evaluateJavaScriptAsync(code2);
String result2 = resultFuture2.get(5, TimeUnit.SECONDS);
Assert.assertEquals(expected1, result1);
Assert.assertEquals(expected2, result2);
}
}
@Test
@MediumTest
public void testTwoIsolatesDoNotShareEnvironment() throws Throwable {
final String code1 = "this.y = 'PASS';\n";
final String expected1 = "PASS";
final String code2 = "this.y = this.y + ' PASS';\n";
final String expected2 = "undefined PASS";
Context context = ContextUtils.getApplicationContext();
ListenableFuture<JavaScriptSandbox> jsSandboxFuture =
JavaScriptSandbox.createConnectedInstanceForTestingAsync(context);
try (JavaScriptSandbox jsSandbox = jsSandboxFuture.get(5, TimeUnit.SECONDS);
JavaScriptIsolate jsIsolate1 = jsSandbox.createIsolate();
JavaScriptIsolate jsIsolate2 = jsSandbox.createIsolate()) {
ListenableFuture<String> resultFuture1 = jsIsolate1.evaluateJavaScriptAsync(code1);
String result1 = resultFuture1.get(5, TimeUnit.SECONDS);
ListenableFuture<String> resultFuture2 = jsIsolate2.evaluateJavaScriptAsync(code2);
String result2 = resultFuture2.get(5, TimeUnit.SECONDS);
Assert.assertEquals(expected1, result1);
Assert.assertEquals(expected2, result2);
}
}
@Test
@MediumTest
public void testTwoExecutionsShareEnvironment() throws Throwable {
final String code1 = "this.z = 'PASS';\n";
final String expected1 = "PASS";
final String code2 = "this.z = this.z + ' PASS';\n";
final String expected2 = "PASS PASS";
Context context = ContextUtils.getApplicationContext();
ListenableFuture<JavaScriptSandbox> jsSandboxFuture =
JavaScriptSandbox.createConnectedInstanceForTestingAsync(context);
try (JavaScriptSandbox jsSandbox = jsSandboxFuture.get(5, TimeUnit.SECONDS);
JavaScriptIsolate jsIsolate1 = jsSandbox.createIsolate()) {
ListenableFuture<String> resultFuture1 = jsIsolate1.evaluateJavaScriptAsync(code1);
String result1 = resultFuture1.get(5, TimeUnit.SECONDS);
ListenableFuture<String> resultFuture2 = jsIsolate1.evaluateJavaScriptAsync(code2);
String result2 = resultFuture2.get(5, TimeUnit.SECONDS);
Assert.assertEquals(expected1, result1);
Assert.assertEquals(expected2, result2);
}
}
@Test
@MediumTest
public void testJsEvaluationError() throws Throwable {
final String code = "throw new WebAssembly.LinkError('RandomLinkError');";
final String contains = "RandomLinkError";
Context context = ContextUtils.getApplicationContext();
ListenableFuture<JavaScriptSandbox> jsSandboxFuture =
JavaScriptSandbox.createConnectedInstanceForTestingAsync(context);
try (JavaScriptSandbox jsSandbox = jsSandboxFuture.get(5, TimeUnit.SECONDS);
JavaScriptIsolate jsIsolate = jsSandbox.createIsolate()) {
ListenableFuture<String> resultFuture = jsIsolate.evaluateJavaScriptAsync(code);
boolean isOfCorrectType = false;
String error = "";
try {
resultFuture.get(5, TimeUnit.SECONDS);
Assert.fail("Should have thrown.");
} catch (ExecutionException e) {
isOfCorrectType = e.getCause().getClass().equals(EvaluationFailedException.class);
error = e.getCause().getMessage();
}
Assert.assertTrue(isOfCorrectType);
Assert.assertTrue(error.contains(contains));
}
}
@Test
@MediumTest
public void testInfiniteLoop() throws Throwable {
final String code = "while(true){}";
Context context = ContextUtils.getApplicationContext();
ListenableFuture<JavaScriptSandbox> jsSandboxFuture =
JavaScriptSandbox.createConnectedInstanceForTestingAsync(context);
try (JavaScriptSandbox jsSandbox = jsSandboxFuture.get(5, TimeUnit.SECONDS)) {
Assume.assumeTrue(
jsSandbox.isFeatureSupported(JavaScriptSandbox.JS_FEATURE_ISOLATE_TERMINATION));
ListenableFuture<String> resultFuture;
try (JavaScriptIsolate jsIsolate = jsSandbox.createIsolate()) {
resultFuture = jsIsolate.evaluateJavaScriptAsync(code);
}
try {
resultFuture.get(5, TimeUnit.SECONDS);
Assert.fail("Should have thrown.");
} catch (ExecutionException e) {
if (!(e.getCause() instanceof IsolateTerminatedException)) {
throw e;
}
}
}
}
@Test
@MediumTest
public void testMultipleInfiniteLoops() throws Throwable {
final String code = "while(true){}";
final int num_of_evaluations = 10;
Context context = ContextUtils.getApplicationContext();
ListenableFuture<JavaScriptSandbox> jsSandboxFuture =
JavaScriptSandbox.createConnectedInstanceForTestingAsync(context);
try (JavaScriptSandbox jsSandbox = jsSandboxFuture.get(5, TimeUnit.SECONDS)) {
Assume.assumeTrue(
jsSandbox.isFeatureSupported(JavaScriptSandbox.JS_FEATURE_ISOLATE_TERMINATION));
Vector<ListenableFuture<String>> resultFutures = new Vector<ListenableFuture<String>>();
try (JavaScriptIsolate jsIsolate = jsSandbox.createIsolate()) {
for (int i = 0; i < num_of_evaluations; i++) {
ListenableFuture<String> resultFuture = jsIsolate.evaluateJavaScriptAsync(code);
resultFutures.add(resultFuture);
}
}
for (int i = 0; i < num_of_evaluations; i++) {
try {
resultFutures.elementAt(i).get(5, TimeUnit.SECONDS);
Assert.fail("Should have thrown.");
} catch (ExecutionException e) {
if (!(e.getCause() instanceof IsolateTerminatedException)) {
throw e;
}
}
}
}
}
@Test
@MediumTest
@DisabledTest(
message =
"Enable it back once we have a WebView version to see if the feature is actually supported in that version")
public void
testFeatureDetection() throws Throwable {
Context context = ContextUtils.getApplicationContext();
ListenableFuture<JavaScriptSandbox> jsSandboxFuture =
JavaScriptSandbox.createConnectedInstanceForTestingAsync(
ContextUtils.getApplicationContext());
try (JavaScriptSandbox jsSandbox = jsSandboxFuture.get(5, TimeUnit.SECONDS)) {
Assert.assertFalse(
jsSandbox.isFeatureSupported(JavaScriptSandbox.JS_FEATURE_ISOLATE_TERMINATION));
}
}
@Test
@MediumTest
public void testSimpleArrayBuffer() throws Throwable {
final String provideString = "Hello World";
final byte[] bytes = provideString.getBytes(StandardCharsets.US_ASCII);
final String code = ""
+ "function ab2str(buf) {"
+ " return String.fromCharCode.apply(null, new Uint8Array(buf));"
+ "}"
+ "android.consumeNamedDataAsArrayBuffer(\"id-1\").then((value) => {"
+ " return ab2str(value);"
+ "});";
Context context = ContextUtils.getApplicationContext();
ListenableFuture<JavaScriptSandbox> jsSandboxFuture =
JavaScriptSandbox.createConnectedInstanceForTestingAsync(context);
try (JavaScriptSandbox jsSandbox = jsSandboxFuture.get(5, TimeUnit.SECONDS);
JavaScriptIsolate jsIsolate = jsSandbox.createIsolate()) {
Assume.assumeTrue(
jsSandbox.isFeatureSupported(JavaScriptSandbox.JS_FEATURE_PROMISE_RETURN));
Assume.assumeTrue(jsSandbox.isFeatureSupported(
JavaScriptSandbox.JS_FEATURE_PROVIDE_CONSUME_ARRAY_BUFFER));
boolean provideNamedDataReturn = jsIsolate.provideNamedData("id-1", bytes);
Assert.assertTrue(provideNamedDataReturn);
ListenableFuture<String> resultFuture1 = jsIsolate.evaluateJavaScriptAsync(code);
String result = resultFuture1.get(5, TimeUnit.SECONDS);
Assert.assertEquals(provideString, result);
}
}
@Test
@MediumTest
public void testArrayBufferWasmCompilation() throws Throwable {
final String success = "success";
// The bytes of a minimal WebAssembly module, courtesy of v8/test/cctest/test-api-wasm.cc
final byte[] bytes = {0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00};
final String code = ""
+ "android.consumeNamedDataAsArrayBuffer(\"id-1\").then((wasm) => {"
+ " return WebAssembly.compile(wasm).then((module) => {"
+ " new WebAssembly.Instance(module);"
+ " return \"success\";"
+ " });"
+ "});";
Context context = ContextUtils.getApplicationContext();
ListenableFuture<JavaScriptSandbox> jsSandboxFuture =
JavaScriptSandbox.createConnectedInstanceForTestingAsync(context);
try (JavaScriptSandbox jsSandbox = jsSandboxFuture.get(5, TimeUnit.SECONDS);
JavaScriptIsolate jsIsolate = jsSandbox.createIsolate()) {
Assume.assumeTrue(
jsSandbox.isFeatureSupported(JavaScriptSandbox.JS_FEATURE_PROMISE_RETURN));
Assume.assumeTrue(jsSandbox.isFeatureSupported(
JavaScriptSandbox.JS_FEATURE_PROVIDE_CONSUME_ARRAY_BUFFER));
Assume.assumeTrue(
jsSandbox.isFeatureSupported(JavaScriptSandbox.JS_FEATURE_WASM_COMPILATION));
boolean provideNamedDataReturn = jsIsolate.provideNamedData("id-1", bytes);
Assert.assertTrue(provideNamedDataReturn);
ListenableFuture<String> resultFuture1 = jsIsolate.evaluateJavaScriptAsync(code);
String result = resultFuture1.get(5, TimeUnit.SECONDS);
Assert.assertEquals(success, result);
}
}
@Test
@MediumTest
public void testPromiseReturn() throws Throwable {
final String code = "Promise.resolve(\"PASS\")";
final String expected = "PASS";
Context context = ContextUtils.getApplicationContext();
ListenableFuture<JavaScriptSandbox> jsSandboxFuture =
JavaScriptSandbox.createConnectedInstanceForTestingAsync(context);
try (JavaScriptSandbox jsSandbox = jsSandboxFuture.get(5, TimeUnit.SECONDS);
JavaScriptIsolate jsIsolate = jsSandbox.createIsolate()) {
Assume.assumeTrue(
jsSandbox.isFeatureSupported(JavaScriptSandbox.JS_FEATURE_PROMISE_RETURN));
ListenableFuture<String> resultFuture = jsIsolate.evaluateJavaScriptAsync(code);
String result = resultFuture.get(5, TimeUnit.SECONDS);
Assert.assertEquals(expected, result);
}
}
@Test
@MediumTest
public void testPromiseReturnLaterResolve() throws Throwable {
final String code1 = "var promiseResolve, promiseReject;"
+ "new Promise(function(resolve, reject){"
+ " promiseResolve = resolve;"
+ " promiseReject = reject;"
+ "});";
final String code2 = "promiseResolve(\"PASS\");";
final String expected = "PASS";
Context context = ContextUtils.getApplicationContext();
ListenableFuture<JavaScriptSandbox> jsSandboxFuture =
JavaScriptSandbox.createConnectedInstanceForTestingAsync(context);
try (JavaScriptSandbox jsSandbox = jsSandboxFuture.get(5, TimeUnit.SECONDS);
JavaScriptIsolate jsIsolate = jsSandbox.createIsolate()) {
Assume.assumeTrue(
jsSandbox.isFeatureSupported(JavaScriptSandbox.JS_FEATURE_PROMISE_RETURN));
ListenableFuture<String> resultFuture1 = jsIsolate.evaluateJavaScriptAsync(code1);
ListenableFuture<String> resultFuture2 = jsIsolate.evaluateJavaScriptAsync(code2);
String result = resultFuture1.get(5, TimeUnit.SECONDS);
Assert.assertEquals(expected, result);
}
}
@Test
@MediumTest
public void testNestedConsumeNamedDataAsArrayBuffer() throws Throwable {
final String success = "success";
// The bytes of a minimal WebAssembly module, courtesy of v8/test/cctest/test-api-wasm.cc
final byte[] bytes = {0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00};
final String code = ""
+ "android.consumeNamedDataAsArrayBuffer(\"id-1\").then((value) => {"
+ " return android.consumeNamedDataAsArrayBuffer(\"id-2\").then((value) => {"
+ " return android.consumeNamedDataAsArrayBuffer(\"id-3\").then((value) => {"
+ " return android.consumeNamedDataAsArrayBuffer(\"id-4\").then((value) => {"
+ " return android.consumeNamedDataAsArrayBuffer(\"id-5\").then((value) => {"
+ " return \"success\";"
+ " }, (error) => {"
+ " return error.message;"
+ " });"
+ " });"
+ " });"
+ " });"
+ "});";
Context context = ContextUtils.getApplicationContext();
ListenableFuture<JavaScriptSandbox> jsSandboxFuture =
JavaScriptSandbox.createConnectedInstanceForTestingAsync(context);
try (JavaScriptSandbox jsSandbox = jsSandboxFuture.get(5, TimeUnit.SECONDS);
JavaScriptIsolate jsIsolate = jsSandbox.createIsolate()) {
Assume.assumeTrue(
jsSandbox.isFeatureSupported(JavaScriptSandbox.JS_FEATURE_PROMISE_RETURN));
Assume.assumeTrue(jsSandbox.isFeatureSupported(
JavaScriptSandbox.JS_FEATURE_PROVIDE_CONSUME_ARRAY_BUFFER));
jsIsolate.provideNamedData("id-1", bytes);
jsIsolate.provideNamedData("id-2", bytes);
jsIsolate.provideNamedData("id-3", bytes);
jsIsolate.provideNamedData("id-4", bytes);
jsIsolate.provideNamedData("id-5", bytes);
ListenableFuture<String> resultFuture1 = jsIsolate.evaluateJavaScriptAsync(code);
String result = resultFuture1.get(5, TimeUnit.SECONDS);
Assert.assertEquals(success, result);
}
}
@Test
@MediumTest
public void testPromiseEvaluationThrow() throws Throwable {
final String provideString = "Hello World";
final byte[] bytes = provideString.getBytes(StandardCharsets.US_ASCII);
final String code = ""
+ "android.consumeNamedDataAsArrayBuffer(\"id-1\").catch((error) => {"
+ " throw new WebAssembly.LinkError('RandomLinkError');"
+ "});";
final String contains = "RandomLinkError";
Context context = ContextUtils.getApplicationContext();
ListenableFuture<JavaScriptSandbox> jsSandboxFuture =
JavaScriptSandbox.createConnectedInstanceForTestingAsync(context);
try (JavaScriptSandbox jsSandbox = jsSandboxFuture.get(5, TimeUnit.SECONDS);
JavaScriptIsolate jsIsolate = jsSandbox.createIsolate()) {
Assume.assumeTrue(
jsSandbox.isFeatureSupported(JavaScriptSandbox.JS_FEATURE_PROMISE_RETURN));
Assume.assumeTrue(jsSandbox.isFeatureSupported(
JavaScriptSandbox.JS_FEATURE_PROVIDE_CONSUME_ARRAY_BUFFER));
ListenableFuture<String> resultFuture = jsIsolate.evaluateJavaScriptAsync(code);
try {
resultFuture.get(5, TimeUnit.SECONDS);
Assert.fail("Should have thrown.");
} catch (ExecutionException e) {
if (!(e.getCause() instanceof EvaluationFailedException)) {
throw e;
}
Assert.assertTrue(e.getCause().getMessage().contains(contains));
}
}
}
@Test
@MediumTest
public void testEvaluationThrowsWhenSandboxClosed() throws Throwable {
final String code = "while(true){}";
Context context = ContextUtils.getApplicationContext();
ListenableFuture<JavaScriptSandbox> jsSandboxFuture =
JavaScriptSandbox.createConnectedInstanceForTestingAsync(context);
try (JavaScriptSandbox jsSandbox = jsSandboxFuture.get(5, TimeUnit.SECONDS);
JavaScriptIsolate jsIsolate = jsSandbox.createIsolate()) {
ListenableFuture<String> resultFuture1 = jsIsolate.evaluateJavaScriptAsync(code);
jsSandbox.close();
// Check already running evaluation gets SandboxDeadException
try {
resultFuture1.get(5, TimeUnit.SECONDS);
Assert.fail("Should have thrown.");
} catch (ExecutionException e) {
if (!(e.getCause() instanceof SandboxDeadException)) {
throw e;
}
}
// Check post-close evaluation gets SandboxDeadException
ListenableFuture<String> resultFuture2 = jsIsolate.evaluateJavaScriptAsync(code);
try {
resultFuture2.get(5, TimeUnit.SECONDS);
Assert.fail("Should have thrown.");
} catch (ExecutionException e) {
if (!(e.getCause() instanceof SandboxDeadException)) {
throw e;
}
}
// Check that closing an isolate then causes the IllegalStateException to be
// thrown instead.
jsIsolate.close();
try {
ListenableFuture<String> postCloseResultFuture =
jsIsolate.evaluateJavaScriptAsync(code);
Assert.fail("Should have thrown.");
} catch (IllegalStateException e) {
// Expected
}
}
}
@Test
@MediumTest
public void testMultipleSandboxesCannotCoexist() throws Throwable {
Context context = ContextUtils.getApplicationContext();
final String contains = "already bound";
ListenableFuture<JavaScriptSandbox> jsSandboxFuture1 =
JavaScriptSandbox.createConnectedInstanceForTestingAsync(context);
try (JavaScriptSandbox jsSandbox1 = jsSandboxFuture1.get(5, TimeUnit.SECONDS)) {
ListenableFuture<JavaScriptSandbox> jsSandboxFuture2 =
JavaScriptSandbox.createConnectedInstanceForTestingAsync(context);
try {
try (JavaScriptSandbox jsSandbox2 = jsSandboxFuture2.get(5, TimeUnit.SECONDS)) {
Assert.fail("Should have thrown.");
}
} catch (ExecutionException e) {
if (!(e.getCause() instanceof RuntimeException)) {
throw e;
}
Assert.assertTrue(e.getCause().getMessage().contains(contains));
}
}
}
@Test
@MediumTest
public void testSandboxCanBeCreatedAfterClosed() throws Throwable {
final String code = "\"PASS\"";
final String expected = "PASS";
final int num_of_startups = 2;
Context context = ContextUtils.getApplicationContext();
for (int i = 0; i < num_of_startups; i++) {
ListenableFuture<JavaScriptSandbox> jsSandboxFuture =
JavaScriptSandbox.createConnectedInstanceForTestingAsync(context);
try (JavaScriptSandbox jsSandbox = jsSandboxFuture.get(5, TimeUnit.SECONDS);
JavaScriptIsolate jsIsolate = jsSandbox.createIsolate()) {
ListenableFuture<String> resultFuture = jsIsolate.evaluateJavaScriptAsync(code);
String result = resultFuture.get(5, TimeUnit.SECONDS);
Assert.assertEquals(expected, result);
}
}
}
@Test
@MediumTest
public void testHeapSizeAdjustment() throws Throwable {
final String code = "\"PASS\"";
final String expected = "PASS";
final long[] heapSizes = {
0,
REASONABLE_HEAP_SIZE,
REASONABLE_HEAP_SIZE - 1,
REASONABLE_HEAP_SIZE + 1,
REASONABLE_HEAP_SIZE + 4095,
REASONABLE_HEAP_SIZE + 4096,
REASONABLE_HEAP_SIZE + 65535,
REASONABLE_HEAP_SIZE + 65536,
1L << 50,
};
Context context = ContextUtils.getApplicationContext();
ListenableFuture<JavaScriptSandbox> jsSandboxFuture =
JavaScriptSandbox.createConnectedInstanceForTestingAsync(context);
try (JavaScriptSandbox jsSandbox = jsSandboxFuture.get(5, TimeUnit.SECONDS)) {
Assume.assumeTrue(jsSandbox.isFeatureSupported(
JavaScriptSandbox.JS_FEATURE_ISOLATE_MAX_HEAP_SIZE));
for (long heapSize : heapSizes) {
IsolateStartupParameters isolateStartupParameters = new IsolateStartupParameters();
isolateStartupParameters.setMaxHeapSizeBytes(heapSize);
try (JavaScriptIsolate jsIsolate =
jsSandbox.createIsolate(isolateStartupParameters)) {
ListenableFuture<String> resultFuture = jsIsolate.evaluateJavaScriptAsync(code);
String result = resultFuture.get(5, TimeUnit.SECONDS);
Assert.assertEquals(expected, result);
} catch (Throwable e) {
throw new AssertionError(
"Failed to evaluate JavaScript using max heap size setting " + heapSize,
e);
}
}
}
}
@Test
@LargeTest
public void testHeapSizeEnforced() throws Throwable {
final long maxHeapSize = REASONABLE_HEAP_SIZE;
// We need to beat the v8 optimizer to ensure it really allocates the required memory. Note
// that we're allocating an array of elements - not bytes. Filling will ensure that the
// array is not sparsely allocated.
final String oomingCode = ""
+ "const array = Array(" + maxHeapSize + ").fill(Math.random(), 0);";
final String stableCode = "'PASS'";
final String stableExpected = "PASS";
final String unresolvedCode = "new Promise((resolve, reject) => {/* never resolve */})";
Context context = ContextUtils.getApplicationContext();
ListenableFuture<JavaScriptSandbox> jsSandboxFuture1 =
JavaScriptSandbox.createConnectedInstanceForTestingAsync(context);
try (JavaScriptSandbox jsSandbox = jsSandboxFuture1.get(5, TimeUnit.SECONDS)) {
Assume.assumeTrue(jsSandbox.isFeatureSupported(
JavaScriptSandbox.JS_FEATURE_ISOLATE_MAX_HEAP_SIZE));
Assume.assumeTrue(
jsSandbox.isFeatureSupported(JavaScriptSandbox.JS_FEATURE_PROMISE_RETURN));
IsolateStartupParameters isolateStartupParameters = new IsolateStartupParameters();
isolateStartupParameters.setMaxHeapSizeBytes(maxHeapSize);
try (JavaScriptIsolate jsIsolate1 = jsSandbox.createIsolate(isolateStartupParameters);
JavaScriptIsolate jsIsolate2 = jsSandbox.createIsolate()) {
ListenableFuture<String> earlyUnresolvedResultFuture =
jsIsolate1.evaluateJavaScriptAsync(unresolvedCode);
ListenableFuture<String> earlyResultFuture =
jsIsolate1.evaluateJavaScriptAsync(stableCode);
ListenableFuture<String> oomResultFuture =
jsIsolate1.evaluateJavaScriptAsync(oomingCode);
// Wait for jsIsolate2 to fully initialize before using jsIsolate1.
jsIsolate2.evaluateJavaScriptAsync(stableCode).get(5, TimeUnit.SECONDS);
// Check that the heap limit is enforced and that it reports this was the evaluation
// that exceeded the limit.
try {
// Use a generous timeout for OOM, as it may involve multiple rounds of garbage
// collection.
oomResultFuture.get(60, TimeUnit.SECONDS);
Assert.fail("Should have thrown.");
} catch (ExecutionException e) {
if (!(e.getCause() instanceof MemoryLimitExceededException)) {
throw e;
}
}
// Check that the previously submitted (but unresolved) promise evaluation reports a
// crash
try {
earlyUnresolvedResultFuture.get(5, TimeUnit.SECONDS);
Assert.fail("Should have thrown.");
} catch (ExecutionException e) {
if (!(e.getCause() instanceof IsolateTerminatedException)) {
throw e;
}
}
// Check that the previously submitted evaluation which completed before the memory
// limit was exceeded, but for which we haven't yet gotten the result, returns its
// result just fine.
String result = earlyResultFuture.get(5, TimeUnit.SECONDS);
Assert.assertEquals(stableExpected, result);
// Check that a totally new evaluation reports a crash
ListenableFuture<String> lateResultFuture =
jsIsolate1.evaluateJavaScriptAsync(stableCode);
try {
lateResultFuture.get(5, TimeUnit.SECONDS);
Assert.fail("Should have thrown.");
} catch (ExecutionException e) {
if (!(e.getCause() instanceof IsolateTerminatedException)) {
throw e;
}
}
// Check that other pre-existing isolates can still be used.
ListenableFuture<String> otherIsolateResultFuture =
jsIsolate2.evaluateJavaScriptAsync(stableCode);
String otherIsolateResult = otherIsolateResultFuture.get(5, TimeUnit.SECONDS);
Assert.assertEquals(stableExpected, otherIsolateResult);
}
}
}
@Test
@LargeTest
public void testIsolateCreationAfterCrash() throws Throwable {
final long maxHeapSize = REASONABLE_HEAP_SIZE;
// We need to beat the v8 optimizer to ensure it really allocates the required memory. Note
// that we're allocating an array of elements - not bytes. Filling will ensure that the
// array is not sparsely allocated.
final String oomingCode = ""
+ "const array = Array(" + maxHeapSize + ").fill(Math.random(), 0);";
final String stableCode = "'PASS'";
final String stableExpected = "PASS";
Context context = ContextUtils.getApplicationContext();
ListenableFuture<JavaScriptSandbox> jsSandboxFuture1 =
JavaScriptSandbox.createConnectedInstanceForTestingAsync(context);
try (JavaScriptSandbox jsSandbox = jsSandboxFuture1.get(5, TimeUnit.SECONDS)) {
Assume.assumeTrue(jsSandbox.isFeatureSupported(
JavaScriptSandbox.JS_FEATURE_ISOLATE_MAX_HEAP_SIZE));
Assume.assumeTrue(
jsSandbox.isFeatureSupported(JavaScriptSandbox.JS_FEATURE_PROMISE_RETURN));
IsolateStartupParameters isolateStartupParameters = new IsolateStartupParameters();
isolateStartupParameters.setMaxHeapSizeBytes(maxHeapSize);
try (JavaScriptIsolate jsIsolate1 = jsSandbox.createIsolate(isolateStartupParameters)) {
ListenableFuture<String> oomResultFuture =
jsIsolate1.evaluateJavaScriptAsync(oomingCode);
// Check that the heap limit is enforced and that it reports this was the evaluation
// that exceeded the limit.
try {
// Use a generous timeout for OOM, as it may involve multiple rounds of garbage
// collection.
oomResultFuture.get(60, TimeUnit.SECONDS);
Assert.fail("Should have thrown.");
} catch (ExecutionException e) {
if (!(e.getCause() instanceof MemoryLimitExceededException)) {
throw e;
}
}
// Check that other isolates can still be created and used (without closing
// jsIsolate1).
try (JavaScriptIsolate jsIsolate2 =
jsSandbox.createIsolate(isolateStartupParameters)) {
ListenableFuture<String> resultFuture =
jsIsolate2.evaluateJavaScriptAsync(stableCode);
String result = resultFuture.get(5, TimeUnit.SECONDS);
Assert.assertEquals(stableExpected, result);
}
}
// Check that other isolates can still be created and used (after closing jsIsolate1).
try (JavaScriptIsolate jsIsolate = jsSandbox.createIsolate(isolateStartupParameters)) {
ListenableFuture<String> resultFuture =
jsIsolate.evaluateJavaScriptAsync(stableCode);
String result = resultFuture.get(5, TimeUnit.SECONDS);
Assert.assertEquals(stableExpected, result);
}
}
// Check that the old sandbox with the "crashed" isolate can be torn down and that a new
// sandbox and isolate can be spun up.
ListenableFuture<JavaScriptSandbox> jsSandboxFuture2 =
JavaScriptSandbox.createConnectedInstanceForTestingAsync(context);
try (JavaScriptSandbox jsSandbox = jsSandboxFuture2.get(5, TimeUnit.SECONDS);
JavaScriptIsolate jsIsolate = jsSandbox.createIsolate()) {
ListenableFuture<String> resultFuture = jsIsolate.evaluateJavaScriptAsync(stableCode);
String result = resultFuture.get(5, TimeUnit.SECONDS);
Assert.assertEquals(stableExpected, result);
}
}
@Test
@MediumTest
public void testAsyncPromiseCallbacks() throws Throwable {
// Unlike testPromiseReturn and testPromiseEvaluationThrow, this test is guaranteed to
// exercise promises in an asynchronous way, rather than in ways which cause a promise to
// resolve or reject immediately within the v8::Script::Run call.
Context context = ContextUtils.getApplicationContext();
ListenableFuture<JavaScriptSandbox> jsSandboxFuture =
JavaScriptSandbox.createConnectedInstanceForTestingAsync(context);
try (JavaScriptSandbox jsSandbox = jsSandboxFuture.get(5, TimeUnit.SECONDS)) {
Assume.assumeTrue(
jsSandbox.isFeatureSupported(JavaScriptSandbox.JS_FEATURE_PROMISE_RETURN));
Assume.assumeTrue(jsSandbox.isFeatureSupported(
JavaScriptSandbox.JS_FEATURE_PROVIDE_CONSUME_ARRAY_BUFFER));
try (JavaScriptIsolate jsIsolate = jsSandbox.createIsolate()) {
// Set up a promise that we can resolve
final String goodPromiseCode = ""
+ "let ext_resolve;"
+ "new Promise((resolve, reject) => {"
+ " ext_resolve = resolve;"
+ "})";
ListenableFuture<String> goodPromiseFuture =
jsIsolate.evaluateJavaScriptAsync(goodPromiseCode);
// Set up a promise that we can reject
final String badPromiseCode = ""
+ "let ext_reject;"
+ "new Promise((resolve, reject) => {"
+ " ext_reject = reject;"
+ "})";
ListenableFuture<String> badPromiseFuture =
jsIsolate.evaluateJavaScriptAsync(badPromiseCode);
// This acts as a barrier to ensure promise code finishes (to the extent of
// returning the promises) before we ask to evaluate the trigger code - else the
// potentially async `ext_resolve = resolve` (or `ext_reject = reject`) code might
// not have been run or queued yet.
jsIsolate.evaluateJavaScriptAsync("''").get(5, TimeUnit.SECONDS);
// Trigger the resolve and rejection from another evaluation to ensure the promises
// are truly asynchronous.
final String triggerCode = ""
+ "ext_resolve('I should succeed!');"
+ "ext_reject(new Error('I should fail!'));"
+ "'DONE'";
ListenableFuture<String> triggerFuture =
jsIsolate.evaluateJavaScriptAsync(triggerCode);
String triggerResult = triggerFuture.get(5, TimeUnit.SECONDS);
Assert.assertEquals("DONE", triggerResult);
// Check resolve
String goodPromiseResult = goodPromiseFuture.get(5, TimeUnit.SECONDS);
Assert.assertEquals("I should succeed!", goodPromiseResult);
// Check reject
try {
String badPromiseResult = badPromiseFuture.get(5, TimeUnit.SECONDS);
Assert.fail("Should have thrown");
} catch (ExecutionException e) {
if (!(e.getCause() instanceof EvaluationFailedException)) {
throw e;
}
Assert.assertTrue(e.getCause().getMessage().contains("I should fail!"));
}
}
}
}
@Test
@LargeTest
public void testArrayBufferSizeEnforced() throws Throwable {
final long maxHeapSize = REASONABLE_HEAP_SIZE;
// V8 cannot sparsely allocate array buffers, so no fill required.
final String oomingCode = ""
+ "const bigArray = new Float32Array(new ArrayBuffer(" + (maxHeapSize + 1) + "));"
+ "'Unreachable'";
final String stableCode = ""
+ "const smallArray = new Float32Array(new ArrayBuffer(100));"
+ "'PASS'";
final String stableExpected = "PASS";
Context context = ContextUtils.getApplicationContext();
ListenableFuture<JavaScriptSandbox> jsSandboxFuture =
JavaScriptSandbox.createConnectedInstanceForTestingAsync(context);
try (JavaScriptSandbox jsSandbox = jsSandboxFuture.get(5, TimeUnit.SECONDS)) {
Assume.assumeTrue(jsSandbox.isFeatureSupported(
JavaScriptSandbox.JS_FEATURE_ISOLATE_MAX_HEAP_SIZE));
IsolateStartupParameters isolateStartupParameters = new IsolateStartupParameters();
isolateStartupParameters.setMaxHeapSizeBytes(maxHeapSize);
try (JavaScriptIsolate jsIsolate = jsSandbox.createIsolate(isolateStartupParameters)) {
// Check that unserviceable large allocations fail.
ListenableFuture<String> resultFuture1 =
jsIsolate.evaluateJavaScriptAsync(oomingCode);
try {
resultFuture1.get(5, TimeUnit.SECONDS);
Assert.fail("Should have thrown.");
} catch (ExecutionException e) {
if (!(e.getCause() instanceof EvaluationFailedException)) {
throw e;
}
EvaluationFailedException cause = (EvaluationFailedException) e.getCause();
if (!cause.getMessage().startsWith(
"Uncaught RangeError: Array buffer allocation failed")) {
throw e;
}
}
// Check that the same isolate can be used to perform a smaller allocation.
ListenableFuture<String> resultFuture2 =
jsIsolate.evaluateJavaScriptAsync(stableCode);
String result = resultFuture2.get(5, TimeUnit.SECONDS);
Assert.assertEquals(stableExpected, result);
}
}
}
@Test
@LargeTest
public void testGarbageCollection() throws Throwable {
final long maxHeapSize = REASONABLE_HEAP_SIZE;
Context context = ContextUtils.getApplicationContext();
ListenableFuture<JavaScriptSandbox> jsSandboxFuture =
JavaScriptSandbox.createConnectedInstanceForTestingAsync(context);
try (JavaScriptSandbox jsSandbox = jsSandboxFuture.get(5, TimeUnit.SECONDS)) {
Assume.assumeTrue(jsSandbox.isFeatureSupported(
JavaScriptSandbox.JS_FEATURE_ISOLATE_MAX_HEAP_SIZE));
IsolateStartupParameters isolateStartupParameters = new IsolateStartupParameters();
isolateStartupParameters.setMaxHeapSizeBytes(maxHeapSize);
final long num_doubles = 1024 * 1024;
// There may be additional allocation overhead beyond this value.
final long allocation_size = 8 * num_doubles;
final long memoryUseFactor = 2;
final long allocationsToTry = memoryUseFactor * maxHeapSize / allocation_size;
// This test will exercise both the V8 heap and ArrayBuffer-allocated memory. Each will
// have allocations totalling approximately memoryUseFactor times the available memory.
//
// Note that the configured heap limit is not precisely enforced by V8, so we need to go
// comfortably over our specified limit (and not just by an extra allocation).
//
// We use doubles (rather than bytes) to reduce (heap) Array overheads.
try (JavaScriptIsolate jsIsolate = jsSandbox.createIsolate(isolateStartupParameters)) {
final String code = ""
+ "this.arrayLength = " + num_doubles + ";"
+ "this.obj = {"
+ " array: Array(this.arrayLength).fill(Math.random(), 0),"
+ " arraybuffer: new Float64Array(new ArrayBuffer(8 * this.arrayLength)),"
+ "};"
+ "'PASS'";
final String expected = "PASS";
for (int i = 0; i < allocationsToTry; i++) {
ListenableFuture<String> resultFuture = jsIsolate.evaluateJavaScriptAsync(code);
// Execution time will be unstable when GC kicks in, so go with 60 seconds.
String result = resultFuture.get(60, TimeUnit.SECONDS);
Assert.assertEquals(expected, result);
}
}
}
}
@Test
@LargeTest
public void testNamedDataCanBeFreed() throws Throwable {
final long maxHeapSize = REASONABLE_HEAP_SIZE;
Context context = ContextUtils.getApplicationContext();
ListenableFuture<JavaScriptSandbox> jsSandboxFuture =
JavaScriptSandbox.createConnectedInstanceForTestingAsync(context);
try (JavaScriptSandbox jsSandbox = jsSandboxFuture.get(5, TimeUnit.SECONDS)) {
Assume.assumeTrue(jsSandbox.isFeatureSupported(
JavaScriptSandbox.JS_FEATURE_ISOLATE_MAX_HEAP_SIZE));
Assume.assumeTrue(
jsSandbox.isFeatureSupported(JavaScriptSandbox.JS_FEATURE_PROMISE_RETURN));
Assume.assumeTrue(jsSandbox.isFeatureSupported(
JavaScriptSandbox.JS_FEATURE_PROVIDE_CONSUME_ARRAY_BUFFER));
IsolateStartupParameters isolateStartupParameters = new IsolateStartupParameters();
isolateStartupParameters.setMaxHeapSizeBytes(maxHeapSize);
// There will be named data allocations of approximately memoryUseFactor times the
// available memory. Note that the memory usage in the Java side is constant with
// respect to number of allocations as we reuse the same input bytes.
try (JavaScriptIsolate jsIsolate = jsSandbox.createIsolate(isolateStartupParameters)) {
final byte[] bytes = new byte[LARGE_NAMED_DATA_SIZE];
final long memoryUseFactor = 2;
final long allocationsToTry = memoryUseFactor * maxHeapSize / LARGE_NAMED_DATA_SIZE;
for (int i = 0; i < allocationsToTry; i++) {
boolean provideNamedDataReturn = jsIsolate.provideNamedData("id-" + i, bytes);
Assert.assertTrue(provideNamedDataReturn);
final String code = ""
+ "android.consumeNamedDataAsArrayBuffer('id-' + " + i + ")"
+ " .then((arrayBuffer) => {"
+ " const len = arrayBuffer.byteLength;"
+ " if (len != " + LARGE_NAMED_DATA_SIZE + ") {"
+ " throw new Error('Bad length ' + len);"
+ " }"
+ " return 'PASS';"
+ " })";
final String expected = "PASS";
ListenableFuture<String> resultFuture = jsIsolate.evaluateJavaScriptAsync(code);
// Execution time may be unstable if the GC kicks in, so go with 60 seconds.
String result = resultFuture.get(60, TimeUnit.SECONDS);
Assert.assertEquals(expected, result);
}
}
}
}
@Test
@LargeTest
public void testNamedDataCanTriggerGarbageCollection() throws Throwable {
// Array buffers for named data are created differently to ordinary array buffers (see
// native service code android_webview::JsSandboxIsolate::tryAllocateArrayBuffer). The
// special allocation code needs to run the garbage collector if we've run out of external
// memory budget. We test this by using up the budget with array buffers (such that there
// would not be enough space to consume the named data), and then forget about (turn into
// garbage) enough buffer memory for the allocation to succeed. This means that when we run
// our own allocation code, there is only enough memory to proceed after a garbage
// collection (but not without one).
final long maxHeapSize = REASONABLE_HEAP_SIZE;
final byte[] bytes = new byte[LARGE_NAMED_DATA_SIZE];
final long allocationsToTry = (maxHeapSize / LARGE_NAMED_DATA_SIZE) + 1;
final String code = ""
+ "const allocation_size = " + LARGE_NAMED_DATA_SIZE + ";"
+ "this.array_buffers = new Array(" + allocationsToTry + ");"
+ "let i;"
+ "for (i = 0; i < this.array_buffers.length; i++) {"
+ " try {"
+ " this.array_buffers[i] = new ArrayBuffer(allocation_size);"
+ " } catch (e) {"
+ " if (e instanceof RangeError) {"
+ " break;"
+ " }"
+ " }"
+ "}"
+ "if (i == this.array_buffers.length) {"
+ " throw new Error('Expected to run out of memory, but did not');"
+ "} else if (i == 0) {"
+ " throw new Error('Could not achieve at least one allocation');"
+ "}"
+ "this.array_buffers[0] = null;"
+ "android.consumeNamedDataAsArrayBuffer('test')"
+ " .then((arrayBuffer) => {"
+ " const len = arrayBuffer.byteLength;"
+ " if (len != " + LARGE_NAMED_DATA_SIZE + ") {"
+ " throw new Error('Bad length ' + len);"
+ " }"
+ " return 'PASS';"
+ " })";
final String expected = "PASS";
Context context = ContextUtils.getApplicationContext();
ListenableFuture<JavaScriptSandbox> jsSandboxFuture =
JavaScriptSandbox.createConnectedInstanceForTestingAsync(context);
try (JavaScriptSandbox jsSandbox = jsSandboxFuture.get(5, TimeUnit.SECONDS)) {
Assume.assumeTrue(jsSandbox.isFeatureSupported(
JavaScriptSandbox.JS_FEATURE_ISOLATE_MAX_HEAP_SIZE));
Assume.assumeTrue(
jsSandbox.isFeatureSupported(JavaScriptSandbox.JS_FEATURE_PROMISE_RETURN));
Assume.assumeTrue(jsSandbox.isFeatureSupported(
JavaScriptSandbox.JS_FEATURE_PROVIDE_CONSUME_ARRAY_BUFFER));
IsolateStartupParameters isolateStartupParameters = new IsolateStartupParameters();
isolateStartupParameters.setMaxHeapSizeBytes(maxHeapSize);
try (JavaScriptIsolate jsIsolate = jsSandbox.createIsolate(isolateStartupParameters)) {
boolean provideNamedDataReturn = jsIsolate.provideNamedData("test", bytes);
Assert.assertTrue(provideNamedDataReturn);
ListenableFuture<String> resultFuture = jsIsolate.evaluateJavaScriptAsync(code);
// Execution time may be unstable if the GC kicks in, so go with 60 seconds.
String result = resultFuture.get(60, TimeUnit.SECONDS);
Assert.assertEquals(expected, result);
}
}
}
@Test
@LargeTest
public void testArrayBuffersAllocatedInPages() throws Throwable {
final long maxHeapSize = REASONABLE_HEAP_SIZE;
Context context = ContextUtils.getApplicationContext();
ListenableFuture<JavaScriptSandbox> jsSandboxFuture =
JavaScriptSandbox.createConnectedInstanceForTestingAsync(context);
try (JavaScriptSandbox jsSandbox = jsSandboxFuture.get(5, TimeUnit.SECONDS)) {
Assume.assumeTrue(jsSandbox.isFeatureSupported(
JavaScriptSandbox.JS_FEATURE_ISOLATE_MAX_HEAP_SIZE));
IsolateStartupParameters isolateStartupParameters = new IsolateStartupParameters();
isolateStartupParameters.setMaxHeapSizeBytes(maxHeapSize);
// The service code should assume that small allocations have overhead and will cost at
// least a 4096 byte page size. (Even if the page size was larger, that shouldn't
// interfere with the accuracy of the test.)
try (JavaScriptIsolate jsIsolate = jsSandbox.createIsolate(isolateStartupParameters)) {
final long allocations = maxHeapSize / 4096 + 1;
// We need to use `new Uint8Array(new ArrayBuffer(1))` instead of `new
// Uint8Array(1)` because V8 appears to instead internalize (onto the V8 heap)
// smaller directly constructed typed arrays.
final String code = ""
+ "(function(){"
+ " const arrayLength = " + allocations + ";"
+ " const buffers = Array(arrayLength);"
+ " for (let i = 0; i < arrayLength; i++) {"
+ " try {"
+ " buffers[i] = new Uint8Array(new ArrayBuffer(1));"
+ " } catch (e) {"
+ " if (e instanceof RangeError) {"
+ " return i;"
+ " } else {"
+ " throw e;"
+ " }"
+ " }"
+ " }"
+ " return 'FAIL';"
+ "})()";
final String notExpected = "FAIL";
ListenableFuture<String> resultFuture = jsIsolate.evaluateJavaScriptAsync(code);
String result = resultFuture.get(60, TimeUnit.SECONDS);
Assert.assertNotEquals(notExpected, result);
}
// Allocating one larger contiguous array buffer should not incur significant overhead.
try (JavaScriptIsolate jsIsolate = jsSandbox.createIsolate(isolateStartupParameters)) {
// At most maxHeapSize is available to array buffers, but maybe less. Go with
// something less than the full limit, but which is still much more than what was
// logically requested by the many smaller buffers.
final long size = maxHeapSize / 8;
final String code = ""
+ "const buffer = new Uint8Array(new ArrayBuffer(" + size + "));"
+ "'PASS'";
final String expected = "PASS";
ListenableFuture<String> resultFuture = jsIsolate.evaluateJavaScriptAsync(code);
String result = resultFuture.get(10, TimeUnit.SECONDS);
Assert.assertEquals(expected, result);
}
}
}
@Test
@LargeTest
public void testOversizedNamedData() throws Throwable {
final long maxHeapSize = REASONABLE_HEAP_SIZE;
final long largeSize = (maxHeapSize + 1L);
Assert.assertTrue(largeSize <= Integer.MAX_VALUE);
final byte[] largeBytes = new byte[(int) largeSize];
final String provideString = "Hello World";
final byte[] smallBytes = provideString.getBytes(StandardCharsets.US_ASCII);
// Test that attempting to consume an oversized named data into a new array buffer fails
// with a RangeError, and a subsequent smaller request succeeds.
final String code = ""
+ "function ab2str(buf) {"
+ " return String.fromCharCode.apply(null, new Uint8Array(buf));"
+ "}"
+ "async function test() {"
+ " try {"
+ " await android.consumeNamedDataAsArrayBuffer('large');"
+ " throw new Error('consumption of large named data should not have succeeded');"
+ " } catch (e) {"
+ " if (!(e instanceof RangeError)) {"
+ " throw e;"
+ " }"
+ " }"
+ " const buffer = await android.consumeNamedDataAsArrayBuffer('small');"
+ " return await ab2str(buffer);"
+ "}"
+ "test()";
Context context = ContextUtils.getApplicationContext();
ListenableFuture<JavaScriptSandbox> jsSandboxFuture =
JavaScriptSandbox.createConnectedInstanceForTestingAsync(context);
try (JavaScriptSandbox jsSandbox = jsSandboxFuture.get(5, TimeUnit.SECONDS)) {
Assume.assumeTrue(jsSandbox.isFeatureSupported(
JavaScriptSandbox.JS_FEATURE_ISOLATE_MAX_HEAP_SIZE));
Assume.assumeTrue(
jsSandbox.isFeatureSupported(JavaScriptSandbox.JS_FEATURE_PROMISE_RETURN));
Assume.assumeTrue(jsSandbox.isFeatureSupported(
JavaScriptSandbox.JS_FEATURE_PROVIDE_CONSUME_ARRAY_BUFFER));
IsolateStartupParameters isolateStartupParameters = new IsolateStartupParameters();
isolateStartupParameters.setMaxHeapSizeBytes(maxHeapSize);
try (JavaScriptIsolate jsIsolate = jsSandbox.createIsolate(isolateStartupParameters)) {
boolean provideNamedDataLargeReturn =
jsIsolate.provideNamedData("large", largeBytes);
Assert.assertTrue(provideNamedDataLargeReturn);
boolean provideNamedDataSmallReturn =
jsIsolate.provideNamedData("small", smallBytes);
Assert.assertTrue(provideNamedDataSmallReturn);
ListenableFuture<String> resultFuture = jsIsolate.evaluateJavaScriptAsync(code);
String result = resultFuture.get(5, TimeUnit.SECONDS);
Assert.assertEquals(provideString, result);
}
}
}
@Test
@LargeTest
public void testUnconsumedNamedData() throws Throwable {
// Ensure that creating and discarding loads of separate unconsumed named data do not result
// in leaks (particularly memory, file descriptors, and threads).
final byte[] bytes = new byte[LARGE_NAMED_DATA_SIZE];
final int numIsolates = 100;
final int numNames = 100;
Context context = ContextUtils.getApplicationContext();
ListenableFuture<JavaScriptSandbox> jsSandboxFuture =
JavaScriptSandbox.createConnectedInstanceForTestingAsync(context);
try (JavaScriptSandbox jsSandbox = jsSandboxFuture.get(5, TimeUnit.SECONDS)) {
Assume.assumeTrue(jsSandbox.isFeatureSupported(
JavaScriptSandbox.JS_FEATURE_PROVIDE_CONSUME_ARRAY_BUFFER));
for (int i = 0; i < numIsolates; i++) {
try (JavaScriptIsolate jsIsolate = jsSandbox.createIsolate()) {
for (int j = 0; j < numNames; j++) {
boolean provideNamedDataReturn =
jsIsolate.provideNamedData("id-" + j, bytes);
Assert.assertTrue(provideNamedDataReturn);
}
}
}
}
}
@Test
@LargeTest
public void testLargeScriptJsEvaluation() throws Throwable {
String longString = "a".repeat(2000000);
final String code = ""
+ "let " + longString + " = 0;"
+ "\"PASS\"";
final String expected = "PASS";
Context context = ContextUtils.getApplicationContext();
ListenableFuture<JavaScriptSandbox> jsSandboxFuture =
JavaScriptSandbox.createConnectedInstanceForTestingAsync(context);
try (JavaScriptSandbox jsSandbox = jsSandboxFuture.get(5, TimeUnit.SECONDS)) {
Assume.assumeTrue(jsSandbox.isFeatureSupported(
JavaScriptSandbox.JS_FEATURE_EVALUATE_WITHOUT_TRANSACTION_LIMIT));
try (JavaScriptIsolate jsIsolate = jsSandbox.createIsolate()) {
ListenableFuture<String> resultFuture = jsIsolate.evaluateJavaScriptAsync(code);
String result = resultFuture.get(10, TimeUnit.SECONDS);
Assert.assertEquals(expected, result);
}
}
}
@Test
@LargeTest
public void testLargeScriptByteArrayJsEvaluation() throws Throwable {
final String longString = "a".repeat(2000000);
final String codeString = ""
+ "let " + longString + " = 0;"
+ "\"PASS\"";
final byte[] code = codeString.getBytes(StandardCharsets.UTF_8);
final String expected = "PASS";
Context context = ContextUtils.getApplicationContext();
ListenableFuture<JavaScriptSandbox> jsSandboxFuture =
JavaScriptSandbox.createConnectedInstanceForTestingAsync(context);
try (JavaScriptSandbox jsSandbox = jsSandboxFuture.get(5, TimeUnit.SECONDS)) {
Assume.assumeTrue(jsSandbox.isFeatureSupported(
JavaScriptSandbox.JS_FEATURE_EVALUATE_WITHOUT_TRANSACTION_LIMIT));
try (JavaScriptIsolate jsIsolate = jsSandbox.createIsolate()) {
ListenableFuture<String> resultFuture = jsIsolate.evaluateJavaScriptAsync(code);
String result = resultFuture.get(10, TimeUnit.SECONDS);
Assert.assertEquals(expected, result);
}
}
}
@Test
@LargeTest
public void testLargeReturn() throws Throwable {
final String longString = "a".repeat(2000000);
final String code = "'a'.repeat(2000000);";
final String expected = longString;
Context context = ContextUtils.getApplicationContext();
ListenableFuture<JavaScriptSandbox> jsSandboxFuture =
JavaScriptSandbox.createConnectedInstanceForTestingAsync(context);
try (JavaScriptSandbox jsSandbox = jsSandboxFuture.get(5, TimeUnit.SECONDS)) {
Assume.assumeTrue(jsSandbox.isFeatureSupported(
JavaScriptSandbox.JS_FEATURE_EVALUATE_WITHOUT_TRANSACTION_LIMIT));
try (JavaScriptIsolate jsIsolate = jsSandbox.createIsolate()) {
ListenableFuture<String> resultFuture = jsIsolate.evaluateJavaScriptAsync(code);
String result = resultFuture.get(60, TimeUnit.SECONDS);
Assert.assertEquals(expected, result);
}
}
}
@Test
@LargeTest
public void testLargeError() throws Throwable {
final String longString = "a".repeat(2000000);
final String code = "throw \"" + longString + "\");";
Context context = ContextUtils.getApplicationContext();
ListenableFuture<JavaScriptSandbox> jsSandboxFuture =
JavaScriptSandbox.createConnectedInstanceForTestingAsync(context);
try (JavaScriptSandbox jsSandbox = jsSandboxFuture.get(5, TimeUnit.SECONDS)) {
Assume.assumeTrue(jsSandbox.isFeatureSupported(
JavaScriptSandbox.JS_FEATURE_EVALUATE_WITHOUT_TRANSACTION_LIMIT));
try (JavaScriptIsolate jsIsolate = jsSandbox.createIsolate()) {
ListenableFuture<String> resultFuture = jsIsolate.evaluateJavaScriptAsync(code);
try {
resultFuture.get(5, TimeUnit.SECONDS);
Assert.fail("Should have thrown.");
} catch (ExecutionException e) {
Assert.assertTrue(
e.getCause().getClass().equals(EvaluationFailedException.class));
Assert.assertTrue(e.getCause().getMessage().contains(longString));
}
}
}
}
@Test
@MediumTest
public void testResultSizeEnforced() throws Throwable {
final int maxSize = 100;
Context context = ContextUtils.getApplicationContext();
ListenableFuture<JavaScriptSandbox> jsSandboxFuture =
JavaScriptSandbox.createConnectedInstanceForTestingAsync(context);
try (JavaScriptSandbox jsSandbox = jsSandboxFuture.get(5, TimeUnit.SECONDS)) {
Assume.assumeTrue(jsSandbox.isFeatureSupported(
JavaScriptSandbox.JS_FEATURE_EVALUATE_WITHOUT_TRANSACTION_LIMIT));
IsolateStartupParameters settings = new IsolateStartupParameters();
settings.setMaxEvaluationReturnSizeBytes(maxSize);
try (JavaScriptIsolate jsIsolate = jsSandbox.createIsolate(settings)) {
// Running code that returns greater than `maxSize` number of bytes should throw.
final String greaterThanMaxSizeCode = ""
+ "'a'.repeat(" + (maxSize + 1) + ");";
ListenableFuture<String> greaterThanMaxSizeResultFuture =
jsIsolate.evaluateJavaScriptAsync(greaterThanMaxSizeCode);
try {
greaterThanMaxSizeResultFuture.get(5, TimeUnit.SECONDS);
Assert.fail("Should have thrown.");
} catch (ExecutionException e) {
if (!(e.getCause() instanceof EvaluationResultSizeLimitExceededException)) {
throw e;
}
}
// Running code that returns `maxSize` number of bytes should not throw.
final String maxSizeCode = ""
+ "'a'.repeat(" + maxSize + ");";
final String maxSizeExpected = "a".repeat(maxSize);
ListenableFuture<String> maxSizeResultFuture =
jsIsolate.evaluateJavaScriptAsync(maxSizeCode);
String maxSizeResult = maxSizeResultFuture.get(5, TimeUnit.SECONDS);
Assert.assertEquals(maxSizeExpected, maxSizeResult);
// Running code that returns less than `maxSize` number of bytes should not throw.
final String lessThanMaxSizeCode = ""
+ "'a'.repeat(" + (maxSize - 1) + ");";
final String lessThanMaxSizeExpected = "a".repeat(maxSize - 1);
ListenableFuture<String> lessThanMaxSizeResultFuture =
jsIsolate.evaluateJavaScriptAsync(lessThanMaxSizeCode);
String lessThanMaxSizeResult = lessThanMaxSizeResultFuture.get(5, TimeUnit.SECONDS);
Assert.assertEquals(lessThanMaxSizeExpected, lessThanMaxSizeResult);
}
}
}
@Test
@MediumTest
public void testErrorSizeEnforced() throws Throwable {
final int maxSize = 100;
final Context context = ContextUtils.getApplicationContext();
final ListenableFuture<JavaScriptSandbox> jsSandboxFuture =
JavaScriptSandbox.createConnectedInstanceForTestingAsync(context);
try (JavaScriptSandbox jsSandbox = jsSandboxFuture.get(5, TimeUnit.SECONDS)) {
Assume.assumeTrue(jsSandbox.isFeatureSupported(
JavaScriptSandbox.JS_FEATURE_EVALUATE_WITHOUT_TRANSACTION_LIMIT));
final IsolateStartupParameters settings = new IsolateStartupParameters();
settings.setMaxEvaluationReturnSizeBytes(maxSize);
try (JavaScriptIsolate jsIsolate = jsSandbox.createIsolate(settings)) {
// Errors which exceed the message threshold should preserve their error type but
// not their message.
//
// Don't test boundary cases as the exact error message is not necessarily
// well-defined.
final String largeError = "a".repeat(maxSize + 1);
final String largeErrorCode = "throw '" + largeError + "';";
final ListenableFuture<String> largeErrorResultFuture =
jsIsolate.evaluateJavaScriptAsync(largeErrorCode);
try {
largeErrorResultFuture.get(5, TimeUnit.SECONDS);
Assert.fail("Should have thrown.");
} catch (ExecutionException e) {
// Assert that the error type is preserved (and not replaced by a size error).
Assert.assertTrue(
e.getCause().getClass().equals(EvaluationFailedException.class));
// Assert that some of the error message is preserved...
Assert.assertTrue(e.getCause().getMessage().contains("aaaaaaaaaaaaaaaa"));
// ... but not all of it.
Assert.assertFalse(e.getCause().getMessage().contains(largeError));
final int messageUtf8ByteLength =
e.getCause().getMessage().getBytes(StandardCharsets.UTF_8).length;
// Our truncation may chop off a complete UTF-8 code point (only 1 byte here).
Assert.assertTrue(messageUtf8ByteLength >= maxSize - 1);
Assert.assertTrue(messageUtf8ByteLength <= maxSize);
}
}
}
}
@Test
@MediumTest
public void testUnicodeResult() throws Throwable {
final Context context = ContextUtils.getApplicationContext();
final ListenableFuture<JavaScriptSandbox> jsSandboxFuture =
JavaScriptSandbox.createConnectedInstanceForTestingAsync(context);
try (JavaScriptSandbox jsSandbox = jsSandboxFuture.get(5, TimeUnit.SECONDS);
JavaScriptIsolate jsIsolate = jsSandbox.createIsolate()) {
final ListenableFuture<String> resultFuture =
jsIsolate.evaluateJavaScriptAsync(ASCII_ESCAPED_JS_UNICODE_TEST_STRING);
final String result = resultFuture.get(5, TimeUnit.SECONDS);
Assert.assertEquals(UNICODE_TEST_STRING, result);
}
}
@Test
@MediumTest
public void testUnicodeError() throws Throwable {
final Context context = ContextUtils.getApplicationContext();
final ListenableFuture<JavaScriptSandbox> jsSandboxFuture =
JavaScriptSandbox.createConnectedInstanceForTestingAsync(context);
try (JavaScriptSandbox jsSandbox = jsSandboxFuture.get(5, TimeUnit.SECONDS);
JavaScriptIsolate jsIsolate = jsSandbox.createIsolate()) {
final ListenableFuture<String> resultFuture = jsIsolate.evaluateJavaScriptAsync(
"throw " + ASCII_ESCAPED_JS_UNICODE_TEST_STRING);
try {
resultFuture.get(5, TimeUnit.SECONDS);
Assert.fail("Should have thrown.");
} catch (ExecutionException e) {
Assert.assertTrue(e.getCause().getClass().equals(EvaluationFailedException.class));
Assert.assertTrue(e.getCause().getMessage().contains(UNICODE_TEST_STRING));
}
}
}
@Test
@MediumTest
public void testUnicodeScript() throws Throwable {
final Context context = ContextUtils.getApplicationContext();
final ListenableFuture<JavaScriptSandbox> jsSandboxFuture =
JavaScriptSandbox.createConnectedInstanceForTestingAsync(context);
try (JavaScriptSandbox jsSandbox = jsSandboxFuture.get(5, TimeUnit.SECONDS);
JavaScriptIsolate jsIsolate = jsSandbox.createIsolate()) {
Assume.assumeTrue(jsSandbox.isFeatureSupported(
JavaScriptSandbox.JS_FEATURE_EVALUATE_WITHOUT_TRANSACTION_LIMIT));
// Test evaluation using String
{
final ListenableFuture<String> resultFuture =
jsIsolate.evaluateJavaScriptAsync(JS_UNICODE_TEST_STRING);
final String result = resultFuture.get(5, TimeUnit.SECONDS);
Assert.assertEquals(UNICODE_TEST_STRING, result);
}
// Test evaluation using UTF-8 byte[]
{
final byte[] codeBytes = JS_UNICODE_TEST_STRING.getBytes(StandardCharsets.UTF_8);
final ListenableFuture<String> resultFuture =
jsIsolate.evaluateJavaScriptAsync(codeBytes);
final String result = resultFuture.get(5, TimeUnit.SECONDS);
Assert.assertEquals(UNICODE_TEST_STRING, result);
}
// Assert that the byte[] API treats ISO_8859_1 (Latin-1) encoded Latin-1
// supplement characters as invalid UTF-8. (Replaced by U+FFFD replacement character.)
{
final byte[] codeBytes = "'Hell\u00f3'".getBytes(StandardCharsets.ISO_8859_1);
final ListenableFuture<String> resultFuture =
jsIsolate.evaluateJavaScriptAsync(codeBytes);
final String result = resultFuture.get(5, TimeUnit.SECONDS);
Assert.assertEquals("Hell\ufffd", result);
}
}
}
@Test
@MediumTest
public void testUnicodeConsoleMessage() throws Throwable {
final Context context = ContextUtils.getApplicationContext();
final ListenableFuture<JavaScriptSandbox> jsSandboxFuture =
JavaScriptSandbox.createConnectedInstanceForTestingAsync(context);
try (JavaScriptSandbox jsSandbox = jsSandboxFuture.get(5, TimeUnit.SECONDS);
JavaScriptIsolate jsIsolate = jsSandbox.createIsolate()) {
Assume.assumeTrue(
jsSandbox.isFeatureSupported(JavaScriptSandbox.JS_FEATURE_CONSOLE_MESSAGING));
Assume.assumeTrue(jsSandbox.isFeatureSupported(
JavaScriptSandbox.JS_FEATURE_EVALUATE_WITHOUT_TRANSACTION_LIMIT));
// Test a small console message
{
final String code = "console.log(" + ASCII_ESCAPED_JS_UNICODE_TEST_STRING + ");";
final AtomicReference<String> messageBody = new AtomicReference<String>(null);
final CountDownLatch latch = new CountDownLatch(1);
jsIsolate.setConsoleCallback(new JavaScriptConsoleCallback() {
@Override
public void onConsoleMessage(JavaScriptConsoleCallback.ConsoleMessage message) {
messageBody.set(message.getMessage());
latch.countDown();
}
});
jsIsolate.evaluateJavaScriptAsync(code).get(5, TimeUnit.SECONDS);
Assert.assertTrue(latch.await(2, TimeUnit.SECONDS));
Assert.assertEquals(UNICODE_TEST_STRING, messageBody.get());
}
// Test a large message.
// Test that truncation of Unicode doesn't result in a crash (but ignore exact result).
// The truncation length is not defined as part of the API (or Binder). Just try
// something significantly larger than the typical 1MB Binder memory limit.
// The truncationUpperBound is measured in bytes.
final int truncationUpperBound = 1024 * 1024;
for (int byteOffset = 0; byteOffset < 4; byteOffset++) {
// \ud83d\udc4b (waving hand sign) is 4 bytes in both UTF-8 and UTF-16.
final String longString = "a".repeat(byteOffset)
+ "\ud83d\udc4b".repeat(truncationUpperBound / 4 + 1)
+ "a".repeat(byteOffset);
final String code = "console.log('" + longString + "');";
final AtomicReference<String> messageBody = new AtomicReference<String>(null);
final CountDownLatch latch = new CountDownLatch(1);
jsIsolate.setConsoleCallback(new JavaScriptConsoleCallback() {
@Override
public void onConsoleMessage(JavaScriptConsoleCallback.ConsoleMessage message) {
messageBody.set(message.getMessage());
latch.countDown();
}
});
jsIsolate.evaluateJavaScriptAsync(code).get(5, TimeUnit.SECONDS);
Assert.assertTrue(
"Timeout with byteOffset " + byteOffset, latch.await(2, TimeUnit.SECONDS));
final int messageUtf8ByteLength =
messageBody.get().getBytes(StandardCharsets.UTF_8).length;
Assert.assertTrue("messageUtf8ByteLength too large with byteOffset " + byteOffset,
messageUtf8ByteLength <= truncationUpperBound);
assertStringEndsWithValidCodePoint(messageBody.get());
}
}
}
@Test
@MediumTest
public void testUnicodeErrorTruncation() throws Throwable {
// Test that truncation of Unicode doesn't result in a crash (but ignore exact result).
final int maxSize = 100;
final Context context = ContextUtils.getApplicationContext();
final ListenableFuture<JavaScriptSandbox> jsSandboxFuture =
JavaScriptSandbox.createConnectedInstanceForTestingAsync(context);
try (JavaScriptSandbox jsSandbox = jsSandboxFuture.get(5, TimeUnit.SECONDS)) {
Assume.assumeTrue(jsSandbox.isFeatureSupported(
JavaScriptSandbox.JS_FEATURE_EVALUATE_WITHOUT_TRANSACTION_LIMIT));
final IsolateStartupParameters settings = new IsolateStartupParameters();
settings.setMaxEvaluationReturnSizeBytes(maxSize);
try (JavaScriptIsolate jsIsolate = jsSandbox.createIsolate(settings)) {
for (int byteOffset = 0; byteOffset < 4; byteOffset++) {
final String longString = "a".repeat(byteOffset)
+ "\ud83d\udc4b".repeat(maxSize) + "a".repeat(byteOffset);
final String code = "throw '" + longString + "';";
final ListenableFuture<String> resultFuture =
jsIsolate.evaluateJavaScriptAsync(code);
try {
resultFuture.get(5, TimeUnit.SECONDS);
Assert.fail("Should have thrown with byteOffset " + byteOffset);
} catch (ExecutionException e) {
Assert.assertTrue("Bad exception with byteOffset " + byteOffset,
e.getCause().getClass().equals(EvaluationFailedException.class));
final int messageUtf8ByteLength =
e.getCause().getMessage().getBytes(StandardCharsets.UTF_8).length;
// Our truncation may chop off a complete or incomplete multi-byte code
// point.
Assert.assertTrue(
"messageUtf8ByteLength too small with byteOffset " + byteOffset,
messageUtf8ByteLength >= maxSize - 4);
Assert.assertTrue(
"messageUtf8ByteLength too large with byteOffset " + byteOffset,
messageUtf8ByteLength <= maxSize);
assertStringEndsWithValidCodePoint(e.getCause().getMessage());
}
}
}
}
}
}