/* * Copyright 2022 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package androidx.javascriptengine; import android.content.res.AssetFileDescriptor; import android.os.Binder; import android.os.RemoteException; import android.util.Log; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.RequiresFeature; import androidx.concurrent.futures.CallbackToFutureAdapter; import androidx.javascriptengine.common.Utils; import com.google.common.util.concurrent.ListenableFuture; import org.chromium.android_webview.js_sandbox.common.IJsSandboxConsoleCallback; import org.chromium.android_webview.js_sandbox.common.IJsSandboxIsolate; import org.chromium.android_webview.js_sandbox.common.IJsSandboxIsolateCallback; import org.chromium.android_webview.js_sandbox.common.IJsSandboxIsolateSyncCallback; import java.io.IOException; import java.nio.charset.StandardCharsets; import java.util.HashSet; import java.util.concurrent.Executor; import java.util.concurrent.RejectedExecutionException; import java.util.concurrent.atomic.AtomicBoolean; import javax.annotation.concurrent.GuardedBy; /** * Environment within a {@link JavaScriptSandbox} where Javascript is executed. *

* A single {@link JavaScriptSandbox} process can contain any number of {@link JavaScriptIsolate} * instances where JS can be evaluated independently and in parallel. *

* Each isolate has its own state and JS global object, * and cannot interact with any other isolate through JS APIs. There is only a moderate * security boundary between isolates in a single {@link JavaScriptSandbox}. If the code in one * {@link JavaScriptIsolate} is able to compromise the security of the JS engine then it may be * able to observe or manipulate other isolates, since they run in the same process. For strong * isolation multiple {@link JavaScriptSandbox} processes should be used, but it is not supported * at the moment. Please find the feature request here. *

* Each isolate object must only be used from one thread. */ public final class JavaScriptIsolate implements AutoCloseable { private static final String TAG = "JavaScriptIsolate"; private final Object mSetLock = new Object(); /** * Interface to underlying service-backed implementation. *

* mJsIsolateStub should only be null when the Isolate has been explicitly closed - not when the * isolate has crashed or simply had its pending and future evaluations cancelled. */ @Nullable private IJsSandboxIsolate mJsIsolateStub; private CloseGuardHelper mGuard = CloseGuardHelper.create(); final JavaScriptSandbox mJsSandbox; @Nullable @GuardedBy("mSetLock") private HashSet> mPendingCompleterSet = new HashSet>(); /** * If mSandboxClosed is true, new evaluations will throw this exception asynchronously. *

* Note that if the isolate is closed, IllegalStateException is thrown synchronously instead. */ @Nullable private Exception mExceptionForNewEvaluations; private AtomicBoolean mSandboxClosed = new AtomicBoolean(false); IsolateStartupParameters mStartupParameters; private class IJsSandboxIsolateSyncCallbackStubWrapper extends IJsSandboxIsolateSyncCallback.Stub { private CallbackToFutureAdapter.Completer mCompleter; IJsSandboxIsolateSyncCallbackStubWrapper( CallbackToFutureAdapter.Completer completer) { mCompleter = completer; } @Override public void reportResultWithFd(AssetFileDescriptor afd) { mJsSandbox.mThreadPoolTaskExecutor.execute( () -> { String result; try { result = Utils.readToString(afd, mStartupParameters.getMaxEvaluationReturnSizeBytes(), false); } catch (IOException | UnsupportedOperationException ex) { mCompleter.setException( new JavaScriptException( "Retrieving result failed: " + ex.getMessage())); removePending(mCompleter); return; } catch (IllegalArgumentException ex) { if (ex.getMessage() != null) { mCompleter.setException( new EvaluationResultSizeLimitExceededException( ex.getMessage())); } else { mCompleter.setException( new EvaluationResultSizeLimitExceededException()); } removePending(mCompleter); return; } handleEvaluationResult(mCompleter, result); }); } @Override public void reportErrorWithFd(@ExecutionErrorTypes int type, AssetFileDescriptor afd) { mJsSandbox.mThreadPoolTaskExecutor.execute( () -> { String error; try { error = Utils.readToString(afd, mStartupParameters.getMaxEvaluationReturnSizeBytes(), true); } catch (IOException | UnsupportedOperationException ex) { mCompleter.setException( new JavaScriptException( "Retrieving error failed: " + ex.getMessage())); removePending(mCompleter); return; } handleEvaluationError(mCompleter, type, error); }); } } private class IJsSandboxIsolateCallbackStubWrapper extends IJsSandboxIsolateCallback.Stub { private CallbackToFutureAdapter.Completer mCompleter; IJsSandboxIsolateCallbackStubWrapper(CallbackToFutureAdapter.Completer completer) { mCompleter = completer; } @Override public void reportResult(String result) { handleEvaluationResult(mCompleter, result); } @Override public void reportError(@ExecutionErrorTypes int type, String error) { handleEvaluationError(mCompleter, type, error); } } private static final class JsSandboxConsoleCallbackRelay extends IJsSandboxConsoleCallback.Stub { private final Executor mExecutor; private final JavaScriptConsoleCallback mCallback; JsSandboxConsoleCallbackRelay(Executor executor, JavaScriptConsoleCallback callback) { mExecutor = executor; mCallback = callback; } @Override public void consoleMessage(final int contextGroupId, final int level, final String message, final String source, final int line, final int column, final String trace) { final long identity = Binder.clearCallingIdentity(); try { mExecutor.execute(() -> { if ((level & JavaScriptConsoleCallback.ConsoleMessage.LEVEL_ALL) == 0 || ((level - 1) & level) != 0) { throw new IllegalArgumentException( "invalid console level " + level + " provided by isolate"); } if (message == null) { throw new IllegalArgumentException("null message provided by isolate"); } if (source == null) { throw new IllegalArgumentException("null source provided by isolate"); } mCallback.onConsoleMessage( new JavaScriptConsoleCallback.ConsoleMessage( level, message, source, line, column, trace)); }); } catch (RejectedExecutionException e) { Log.e(TAG, "Console message dropped", e); } finally { Binder.restoreCallingIdentity(identity); } } @Override public void consoleClear(int contextGroupId) { final long identity = Binder.clearCallingIdentity(); try { mExecutor.execute(() -> mCallback.onConsoleClear()); } catch (RejectedExecutionException e) { Log.e(TAG, "Console clear dropped", e); } finally { Binder.restoreCallingIdentity(identity); } } } JavaScriptIsolate(IJsSandboxIsolate jsIsolateStub, JavaScriptSandbox sandbox, IsolateStartupParameters settings) { mJsSandbox = sandbox; mJsIsolateStub = jsIsolateStub; mStartupParameters = settings; mGuard.open("close"); // This should be at the end of the constructor. } /** * Evaluates the given JavaScript code and returns the result. *

* There are 3 possible behaviors based on the output of the expression: *

* The environment uses a single JS global object for all the calls to {@link * #evaluateJavaScriptAsync(String)} and {@link #provideNamedData(String, byte[])} methods. * These calls are queued up and are run one at a time in sequence, using the single JS * environment for the isolate. The global variables set by one evaluation are visible for * later evaluations. This is similar to adding multiple {@code