blob: 0c09480fd92f505fa92984671f44c7b4493fc515 [file] [log] [blame]
/*
* Copyright 2025 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.common;
import android.content.res.AssetFileDescriptor;
import android.os.Binder;
import android.os.DeadObjectException;
import android.os.IBinder;
import android.os.RemoteException;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.google.common.util.concurrent.MoreExecutors;
import org.chromium.android_webview.js_sandbox.common.IMessagePort;
import java.io.EOFException;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.Objects;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.atomic.AtomicReference;
/**
* Internal implementation of a message port, responsible for the low-level mechanics of
* message passing over binder, and implementing the IMessagePort aidl interface.
* <p>
* This class handles the logic for sending and receiving messages. If an instance of
* this class is accessed from multiple threads, callers must ensure proper synchronization.
* <p>
* A MessagePortInternal is one of two ends of a message channel. The other end is in another
* process. To establish the connection, each side creates a MessagePortInternal and sends its
* local binder {@link #getLocalIMessagePort()} to the other side, which then sets it as the remote
* binder using {@link #setRemoteIMessagePort(IMessagePort)}.
* <p>
* Small messages (<64KiB) are sent directly over binder IPC.
* Large messages (>=64KiB) are sent through file descriptors to avoid binder transaction limits.
* The file descriptor itself is sent over binder.
* <p>
* Messages received before a {@link MessagePortClient} is set via
* {@link #setClient(MessagePortClient)} are queued.
* <p>
* The port state (open/closed) is given by the {@link #mRemoteIMessagePort} value.
*/
public final class MessagePortInternal {
private static final int MAX_BINDER_STRING_LENGTH = 32767; // 32 KiB - 1
private static final int MAX_BINDER_ARRAY_BUFFER_SIZE = 65535; // 64 KiB - 1
// Local binder stub that receives calls from the remote process.
// Responsible for receiving messages from the remote port.
@NonNull
public final MessagePortIpcClient mLocalIMessagePort;
// Remote binder proxy to send messages to the other process.
// Dictates the state of the message port. If null port is closed, if otherwise port is open.
// Responsible for sending messages to the remote port.
public final AtomicReference<IMessagePort> mRemoteIMessagePort =
new AtomicReference<>(null);
// Sandbox thread pool task executor.
// Offloads messages sent over file descriptor.
@NonNull
public final ExecutorService mIoExecutor;
// The client is an implementation logic of the functions that determine how the messages
// are processed after they have been received.
// This is not responsible for receiving messages. Is is responsible to process messages.
@NonNull
public MessagePortClient mMessagePortClient;
// Maximum return size of the isolate.
// This applies only to incoming messages on the app side.
// The service side limit is set to an unreachable high value.
private final int mMaxMessageSize;
// Binder link to death. Link closeLocally to isolate/sandbox death.
@Nullable
public IBinder.DeathRecipient mDeathRecipient;
private final class MessagePortIpcClient extends IMessagePort.Stub {
MessagePortIpcClient() {}
@Override
public void sendString(String string) {
Objects.requireNonNull(string);
if (mRemoteIMessagePort.get() == null) return; // Silently discard the message
long identity = Binder.clearCallingIdentity();
try {
mMessagePortClient.onString(string);
} finally {
Binder.restoreCallingIdentity(identity);
}
}
@Override
public void sendStringOverFd(AssetFileDescriptor afd) {
Objects.requireNonNull(afd);
boolean shouldClose = true;
try {
long length = afd.getLength();
if (length < 0) {
throw new IllegalArgumentException("string has invalid length: " + length);
} else if (length > mMaxMessageSize) {
throw new IllegalArgumentException("string is too large: " + length);
}
if (mRemoteIMessagePort.get() == null) return; // Silently discard the message
long identity = Binder.clearCallingIdentity();
try (InputStream inputStream = afd.createInputStream()) {
byte[] bytes = new byte[(int) length];
int bytesRead = Utils.readNBytes(inputStream, bytes, 0, bytes.length);
if (bytesRead != bytes.length) {
throw new EOFException("Incomplete data read from file descriptor."
+ " Expected " + bytes.length + " bytes, but got " + bytesRead);
}
String string = new String(bytes, StandardCharsets.UTF_8);
mMessagePortClient.onString(string);
} catch (IOException e) {
throw new RuntimeException(e);
} finally {
Binder.restoreCallingIdentity(identity);
}
shouldClose = false;
} finally {
if (shouldClose) Utils.closeQuietly(afd);
}
}
@Override
public void sendArrayBuffer(byte[] bytes) {
Objects.requireNonNull(bytes);
if (mRemoteIMessagePort.get() == null) return; // Silently discard the message
long identity = Binder.clearCallingIdentity();
try {
mMessagePortClient.onArrayBuffer(bytes);
} finally {
Binder.restoreCallingIdentity(identity);
}
}
@Override
public void sendArrayBufferOverFd(AssetFileDescriptor afd) {
Objects.requireNonNull(afd);
boolean shouldClose = true;
try {
long length = afd.getLength();
if (length < 0) {
throw new IllegalArgumentException("arrayBuffer has invalid length: " + length);
} else if (length > mMaxMessageSize) {
throw new IllegalArgumentException("arrayBuffer is too large: " + length);
}
if (mRemoteIMessagePort.get() == null) return; // Silently discard the message
long identity = Binder.clearCallingIdentity();
try (InputStream inputStream = afd.createInputStream()) {
byte[] bytes = new byte[(int) length];
int bytesRead = Utils.readNBytes(inputStream, bytes, 0, bytes.length);
if (bytesRead != bytes.length) {
throw new EOFException("Incomplete data read from file descriptor."
+ " Expected " + bytes.length + " bytes, but got " + bytesRead);
}
mMessagePortClient.onArrayBuffer(bytes);
} catch (IOException e) {
throw new RuntimeException(e);
} finally {
Binder.restoreCallingIdentity(identity);
}
shouldClose = false;
} finally {
if (shouldClose) Utils.closeQuietly(afd);
}
}
@Override
public void close() {
MessagePortInternal.this.closeLocally();
}
}
/**
* Interface for handling messages received through a MessagePortInternal.
*/
public interface MessagePortClient {
/**
* Called when a String message is received.
*
* @param string The String message.
*/
void onString(@NonNull String string);
/**
* Called when an ArrayBuffer message is received.
*
* @param arrayBuffer The ArrayBuffer message as a byte array.
*/
void onArrayBuffer(@NonNull byte[] arrayBuffer);
}
/**
* Creates a new MessagePortInternal that can later be entangled.
*
* @param executorService The ExecutorService that will run the pipe reading.
* @param maxMessageSize The maximum size allowed for a received message.
* @return a new MessagePortInternal.
*/
public MessagePortInternal(@NonNull ExecutorService executorService, int maxMessageSize) {
mLocalIMessagePort = new MessagePortIpcClient();
mIoExecutor = executorService;
mMessagePortClient = new MessagePortClient() {
@Override
public void onString(String string) {}
@Override
public void onArrayBuffer(byte[] arrayBuffer) {}
};
mMaxMessageSize = maxMessageSize;
}
/**
* Creates an empty MessagePortInternal that cannot be used.
*
* @return an empty unusable MessagePortInternal.
*/
public MessagePortInternal() {
mLocalIMessagePort = new MessagePortIpcClient();
mIoExecutor = MoreExecutors.newDirectExecutorService();
mMessagePortClient = new MessagePortClient() {
@Override
public void onString(String string) {}
@Override
public void onArrayBuffer(byte[] arrayBuffer) {}
};
mMaxMessageSize = 0;
}
/**
* The local IMessagePort used by the remote port to send messages.
*
* @return the local IMessagePort.
*/
@NonNull
public IMessagePort getLocalIMessagePort() {
return mLocalIMessagePort;
}
/**
* Sets the remote IMessagePort.
*
* Remote IMessagePort is responsible for sending messages.
* <p>
* Must be called exactly once after construction, and before doing
* anything else with the object.
*
* @param iMessagePort the remote IMessagePort.
*/
public void setRemoteIMessagePort(@NonNull IMessagePort iMessagePort) {
mRemoteIMessagePort.set(iMessagePort);
mDeathRecipient = new IBinder.DeathRecipient() {
@Override
public void binderDied() {
closeLocally();
}
};
try {
iMessagePort.asBinder().linkToDeath(mDeathRecipient, 0);
} catch (RemoteException e) {
// Remote already dead. Close local port.
closeLocally();
}
}
/**
* Sets the client for receiving messages.
*
* This should be set exactly once, before exposing the port to the other side.
*
* @param client the client to handle the messages.
*/
public void setClient(@NonNull MessagePortClient client) {
mMessagePortClient = client;
}
/**
* Posts a String message to the remote port.
*
* @param string the String message to post.
*/
public void postString(@NonNull String string) {
IMessagePort remote = mRemoteIMessagePort.get();
if (remote == null) return;
try {
if (string.length() <= MAX_BINDER_STRING_LENGTH) {
remote.sendString(string);
} else {
byte[] bytes = string.getBytes(StandardCharsets.UTF_8);
try (AssetFileDescriptor afd = Utils.writeBytesIntoPipeAsync(bytes, mIoExecutor)) {
remote.sendStringOverFd(afd);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
} catch (DeadObjectException e) {
// The remote process has died, so we can ignore this error.
} catch (RemoteException e) {
throw new RuntimeException(e);
}
}
/**
* Posts an ArrayBuffer message to the remote port.
*
* @param bytes the ArrayBuffer message to post.
*/
public void postArrayBuffer(@NonNull byte[] bytes) {
IMessagePort remote = mRemoteIMessagePort.get();
if (remote == null) return;
try {
if (bytes.length <= MAX_BINDER_ARRAY_BUFFER_SIZE) {
remote.sendArrayBuffer(bytes);
} else {
try (AssetFileDescriptor afd = Utils.writeBytesIntoPipeAsync(bytes, mIoExecutor)) {
remote.sendArrayBufferOverFd(afd);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
} catch (DeadObjectException e) {
// The remote process has died, so we can ignore this error.
} catch (RemoteException e) {
throw new RuntimeException(e);
}
}
/**
* Closes the message port, and notifies the remote port that this end is closed.
*
* Called by the API or on isolate/sandbox close and death.
*/
public void close() {
IMessagePort remoteIMessagePort = mRemoteIMessagePort.getAndSet(null);
if (remoteIMessagePort == null) return;
closeLocally();
try {
remoteIMessagePort.close();
} catch (DeadObjectException e) {
// The remote process has died, so we can ignore this error.
} catch (RemoteException e) {
throw new RuntimeException(e);
}
}
/**
* Handles the closing of the local port and releases its resources.
*
* Called by the API or remote port.
*/
private void closeLocally() {
mRemoteIMessagePort.set(null);
}
}