chromium/android_webview/js_sandbox/service/js_sandbox_isolate.cc

// 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.

#include "android_webview/js_sandbox/service/js_sandbox_isolate.h"

#include <errno.h>
#include <unistd.h>

#include <algorithm>
#include <cstddef>
#include <memory>
#include <set>
#include <string>
#include <string_view>

#include "android_webview/js_sandbox/service/js_sandbox_array_buffer_allocator.h"
#include "android_webview/js_sandbox/service/js_sandbox_isolate_callback.h"
#include "base/android/callback_android.h"
#include "base/android/jni_android.h"
#include "base/android/jni_string.h"
#include "base/check.h"
#include "base/check_op.h"
#include "base/containers/span.h"
#include "base/files/file_util.h"
#include "base/functional/bind.h"
#include "base/immediate_crash.h"
#include "base/logging.h"
#include "base/memory/raw_ptr.h"
#include "base/memory/scoped_refptr.h"
#include "base/notreached.h"
#include "base/numerics/safe_math.h"
#include "base/strings/strcat.h"
#include "base/strings/string_number_conversions.h"
#include "base/synchronization/waitable_event.h"
#include "base/system/sys_info.h"
#include "base/task/cancelable_task_tracker.h"
#include "base/task/single_thread_task_runner.h"
#include "base/task/thread_pool.h"
#include "base/task/thread_pool/thread_pool_instance.h"
#include "base/threading/thread_restrictions.h"
#include "base/time/time.h"
#include "gin/arguments.h"
#include "gin/array_buffer.h"
#include "gin/function_template.h"
#include "gin/public/context_holder.h"
#include "gin/public/isolate_holder.h"
#include "gin/try_catch.h"
#include "gin/v8_initializer.h"
#include "js_sandbox_isolate.h"
#include "v8/include/v8-array-buffer.h"
#include "v8/include/v8-function.h"
#include "v8/include/v8-inspector.h"
#include "v8/include/v8-isolate.h"
#include "v8/include/v8-microtask-queue.h"
#include "v8/include/v8-statistics.h"
#include "v8/include/v8-template.h"

// Must come after all headers that specialize FromJniType() / ToJniType().
#include "android_webview/js_sandbox/js_sandbox_jni_headers/JsSandboxIsolate_jni.h"

using base::android::ConvertJavaStringToUTF8;
using base::android::ConvertUTF8ToJavaString;
using base::android::JavaParamRef;
using base::android::JavaRef;

namespace {

// TODO(crbug.com/40215244): This is what shows up as filename in errors.
// Revisit this once error handling is in place.
constexpr std::string_view resource_name = "<expression>";
constexpr jlong kUnknownAssetFileDescriptorLength = -1;
constexpr int64_t kDefaultChunkSize = 1 << 16;

size_t GetAllocatePageSize() {
  return gin::V8Platform::Get()->GetPageAllocator()->AllocatePageSize();
}

// AdjustToValidHeapSize will either round the provided heap size up to a valid
// allocation page size or clip the value to the maximum supported heap size.
size_t AdjustToValidHeapSize(const size_t heap_size_bytes) {
  // This value is not necessarily the same as the system's memory page
  // size. https://bugs.chromium.org/p/v8/issues/detail?id=13172#c6
  const size_t page_size = GetAllocatePageSize();
  const size_t max_supported_heap_size =
      size_t{UINT_MAX} / page_size * page_size;

  if (heap_size_bytes < max_supported_heap_size) {
    return (heap_size_bytes + (page_size - 1)) / page_size * page_size;
  } else {
    return max_supported_heap_size;
  }
}

// Reads content of an Fd from current position to EOF
// Returns true iff success
// Returns false on failure and sets errno
bool ReadFdToStringTillEof(int fd, std::string& contents) {
  contents.clear();
  char temp_buffer[kDefaultChunkSize];
  int64_t bytes_read_this_pass;

  while ((bytes_read_this_pass =
              HANDLE_EINTR(read(fd, temp_buffer, kDefaultChunkSize))) > 0) {
    contents.append(temp_buffer, 0, bytes_read_this_pass);
  }

  if (bytes_read_this_pass == -1) {
    contents.clear();
    return false;
  }

  contents.shrink_to_fit();
  return true;
}

// Skip bytes in case lseek fails
// Returns -1 on read failure and sets errno
// Otherwise returns number of bytes read, including cases where eof is reached
// early and less than expected bytes are read.
int64_t ReadBytesFromFdAndDiscard(int fd, int64_t bytes_to_read) {
  int64_t bytes_read_this_pass;
  int64_t bytes_read_so_far = 0;
  char local_contents[kDefaultChunkSize];

  while (bytes_read_so_far < bytes_to_read) {
    bytes_read_this_pass = HANDLE_EINTR(
        read(fd, &local_contents[0],
             std::min(bytes_to_read - bytes_read_so_far, kDefaultChunkSize)));

    if (bytes_read_this_pass == -1) {
      return -1;
    } else if (bytes_read_this_pass == 0) {
      // eof is reached early
      return bytes_read_so_far;
    }
    bytes_read_so_far += bytes_read_this_pass;
  }

  return bytes_read_so_far;
}

v8::Local<v8::String> GetSourceLine(v8::Isolate* isolate,
                                    v8::Local<v8::Message> message) {
  auto maybe = message->GetSourceLine(isolate->GetCurrentContext());
  v8::Local<v8::String> source_line;
  return maybe.ToLocal(&source_line) ? source_line : v8::String::Empty(isolate);
}

std::string GetStackTrace(v8::Local<v8::Message>& message,
                          v8::Isolate* isolate) {
  std::stringstream ss;
  ss << gin::V8ToString(isolate, message->Get()) << std::endl
     << gin::V8ToString(isolate, GetSourceLine(isolate, message)) << std::endl;

  v8::Local<v8::StackTrace> trace = message->GetStackTrace();
  if (trace.IsEmpty())
    return ss.str();

  int len = trace->GetFrameCount();
  for (int i = 0; i < len; ++i) {
    v8::Local<v8::StackFrame> frame = trace->GetFrame(isolate, i);
    ss << gin::V8ToString(isolate, frame->GetScriptName()) << ":"
       << frame->GetLineNumber() << ":" << frame->GetColumn() << ": "
       << gin::V8ToString(isolate, frame->GetFunctionName()) << std::endl;
  }
  return ss.str();
}

// Logic borrowed and kept similar to gin::TryCatch::GetStackTrace()
std::string GetStackTrace(v8::TryCatch& try_catch, v8::Isolate* isolate) {
  if (!try_catch.HasCaught()) {
    return "";
  }
  v8::Local<v8::Message> message = try_catch.Message();
  return GetStackTrace(message, isolate);
}

jint remapConsoleMessageErrorLevel(const v8::Isolate::MessageErrorLevel level) {
  // Converted level should match the values specified in the
  // org.chromium.android_webview.js_sandbox.common.IJsSandboxIsolateClient AIDL
  // file (in AndroidX).
  //
  // These will probably remain identical to the underlying v8 enums/constants,
  // but are mapped explicitly here to ensure we can maintain compatibility even
  // if there are changes or additions.
  switch (level) {
    case v8::Isolate::MessageErrorLevel::kMessageLog:
      return 1 << 0;
    case v8::Isolate::MessageErrorLevel::kMessageDebug:
      return 1 << 1;
    case v8::Isolate::MessageErrorLevel::kMessageInfo:
      return 1 << 2;
    case v8::Isolate::MessageErrorLevel::kMessageError:
      return 1 << 3;
    case v8::Isolate::MessageErrorLevel::kMessageWarning:
      return 1 << 4;
    case v8::Isolate::MessageErrorLevel::kMessageAll:
      NOTREACHED();
  }
}

// Converts a V8 inspector (UTF-8 or UTF-16) StringView to a jstring.
base::android::ScopedJavaLocalRef<jstring> StringViewToJavaString(
    JNIEnv* const env,
    const v8_inspector::StringView& string_view) {
  if (string_view.is8Bit()) {
    return base::android::ConvertUTF8ToJavaString(
        env, std::string_view(
                 reinterpret_cast<const char*>(string_view.characters8()),
                 string_view.length()));
  } else {
    return base::android::ConvertUTF16ToJavaString(
        env, std::u16string_view(
                 reinterpret_cast<const char16_t*>(string_view.characters16()),
                 string_view.length()));
  }
}

class NoopInspectorChannel final : public v8_inspector::V8Inspector::Channel {
 public:
  ~NoopInspectorChannel() override = default;
  void sendResponse(
      int callId,
      std::unique_ptr<v8_inspector::StringBuffer> message) override {}
  void sendNotification(
      std::unique_ptr<v8_inspector::StringBuffer> message) override {}
  void flushProtocolNotifications() override {}
};

// This must match the values defined in IJsSandboxIsolateInstanceCallback's
// TERMINATED_ constants;
enum class TerminationStatus {
  kUnknownError = 1,
  kSandboxDead = 2,
  kMemoryLimitExceeded = 3,
};

}  // namespace

namespace android_webview {

class FdWithLength {
 public:
  base::ScopedFD fd;
  ssize_t length;

  FdWithLength(int fd, ssize_t len);
  ~FdWithLength() = default;
  FdWithLength(FdWithLength&&) = default;
  FdWithLength& operator=(FdWithLength&&) = default;
};

FdWithLength::FdWithLength(int fd_input, ssize_t len) {
  fd = base::ScopedFD(fd_input);
  length = len;
}

// This class must only be constructed, destructed, and used from the isolate
// thread.
class JsSandboxIsolate::InspectorClient final
    : public v8_inspector::V8InspectorClient {
 public:
  explicit InspectorClient(JsSandboxIsolate& isolate) : isolate_(isolate) {}

  ~InspectorClient() override = default;

  void consoleAPIMessage(const int context_group_id,
                         const v8::Isolate::MessageErrorLevel level,
                         const v8_inspector::StringView& message,
                         const v8_inspector::StringView& url,
                         const unsigned int line_number,
                         const unsigned int column_number,
                         v8_inspector::V8StackTrace* const trace) override {
    if (!isolate_->console_enabled_) {
      return;
    }

    JNIEnv* env = base::android::AttachCurrentThread();
    const jint converted_level = remapConsoleMessageErrorLevel(level);
    base::android::ScopedJavaLocalRef<jstring> java_string_message =
        StringViewToJavaString(env, message);
    // url is actually just the source (file/expression) identifier.
    base::android::ScopedJavaLocalRef<jstring> java_string_source =
        StringViewToJavaString(env, url);
    base::android::ScopedJavaLocalRef<jstring> java_string_trace;
    if (trace && !trace->isEmpty()) {
      StringViewToJavaString(env, trace->toString()->string());
    }

    android_webview::Java_JsSandboxIsolate_consoleMessage(
        env, isolate_->j_isolate_, static_cast<jint>(context_group_id),
        converted_level, java_string_message, java_string_source,
        base::saturated_cast<jint>(line_number),
        base::saturated_cast<jint>(column_number), java_string_trace);
  }

  void consoleClear(const int context_group_id) override {
    if (!isolate_->console_enabled_) {
      return;
    }
    JNIEnv* env = base::android::AttachCurrentThread();
    android_webview::Java_JsSandboxIsolate_consoleClear(
        env, isolate_->j_isolate_, static_cast<jint>(context_group_id));
  }

  double currentTimeMS() override {
    // Note: although this is not monotonically increasing time, this reflects
    // the behaviour of Blink code.
    return base::Time::Now().InMillisecondsFSinceUnixEpoch();
  }

 private:
  const raw_ref<JsSandboxIsolate> isolate_;
};

JsSandboxIsolate::JsSandboxIsolate(
    const base::android::JavaParamRef<jobject>& j_isolate,
    const size_t max_heap_size_bytes)
    : j_isolate_(j_isolate),
      isolate_max_heap_size_bytes_(max_heap_size_bytes),
      array_buffer_allocator_(std::make_unique<JsSandboxArrayBufferAllocator>(
          *gin::ArrayBufferAllocator::SharedInstance(),
          max_heap_size_bytes > 0
              ? max_heap_size_bytes
              : JsSandboxArrayBufferAllocator::kUnlimitedBudget,
          // This is a bit of an implementation detail - gin uses the same
          // underlying allocator for pages and array buffers.
          GetAllocatePageSize())),
      control_task_runner_(base::ThreadPool::CreateSequencedTaskRunner({})),
      isolate_task_runner_(base::ThreadPool::CreateSingleThreadTaskRunner(
          {base::TaskPriority::USER_BLOCKING,
           base::TaskShutdownBehavior::SKIP_ON_SHUTDOWN, base::MayBlock()},
          base::SingleThreadTaskRunnerThreadMode::DEDICATED)) {
  control_task_runner_->PostTask(
      FROM_HERE, base::BindOnce(&JsSandboxIsolate::CreateCancelableTaskTracker,
                                base::Unretained(this)));
  isolate_task_runner_->PostTask(
      FROM_HERE, base::BindOnce(&JsSandboxIsolate::InitializeIsolateOnThread,
                                base::Unretained(this)));
}

JsSandboxIsolate::~JsSandboxIsolate() {}

// Called from Binder thread.
// This method posts evaluation tasks to the control_task_runner_. The
// control_task_runner_ provides ordering to the requests and manages
// cancelable_task_tracker_ which allows us to cancel tasks. The
// control_task_runner_ in turn posts tasks via cancelable_task_tracker_ to the
// isolate_task_runner_ which interacts with the isolate and runs the evaluation
// in v8. Only isolate_task_runner_ should be used to interact with the isolate
// for thread-affine v8 APIs. The callback is invoked from the
// isolate_task_runner_.
jboolean JsSandboxIsolate::EvaluateJavascript(
    JNIEnv* env,
    const base::android::JavaParamRef<jobject>& obj,
    const base::android::JavaParamRef<jstring>& jcode,
    const base::android::JavaParamRef<jobject>& j_callback) {
  std::string code = ConvertJavaStringToUTF8(env, jcode);
  scoped_refptr<JsSandboxIsolateCallback> callback =
      base::MakeRefCounted<JsSandboxIsolateCallback>(
          base::android::ScopedJavaGlobalRef<jobject>(j_callback), false);
  control_task_runner_->PostTask(
      FROM_HERE,
      base::BindOnce(&JsSandboxIsolate::PostEvaluationToIsolateThread,
                     base::Unretained(this), std::move(code),
                     std::move(callback)));
  return true;
}

// Called from Binder thread.
// Refer to comment above EvaluateJavascript method. In addition, this method
// checks for streaming failures.
jboolean JsSandboxIsolate::EvaluateJavascriptWithFd(
    JNIEnv* env,
    const base::android::JavaParamRef<jobject>& obj,
    const jint fd,
    const jlong length,
    const jlong offset,
    const base::android::JavaParamRef<jobject>& j_callback,
    const base::android::JavaParamRef<jobject>& j_pfd) {
  scoped_refptr<JsSandboxIsolateCallback> callback =
      base::MakeRefCounted<JsSandboxIsolateCallback>(
          base::android::ScopedJavaGlobalRef<jobject>(j_callback), true);

  control_task_runner_->PostTask(
      FROM_HERE,
      base::BindOnce(&JsSandboxIsolate::PostFileDescriptorReadToIsolateThread,
                     base::Unretained(this), fd, length, offset,
                     base::android::ScopedJavaGlobalRef<jobject>(j_pfd),
                     std::move(callback)));

  return true;
}

// Called from Binder thread.
void JsSandboxIsolate::DestroyNative(
    JNIEnv* env,
    const base::android::JavaParamRef<jobject>& obj) {
  control_task_runner_->PostTask(
      FROM_HERE, base::BindOnce(&JsSandboxIsolate::DestroyWhenPossible,
                                base::Unretained(this)));
}

// Called from Binder thread.
jboolean JsSandboxIsolate::ProvideNamedData(
    JNIEnv* env,
    const base::android::JavaParamRef<jobject>& obj,
    const base::android::JavaParamRef<jstring>& jname,
    const jint fd,
    const jint length) {
  std::string name = ConvertJavaStringToUTF8(env, jname);
  base::AutoLock hold(named_fd_lock_);
  FdWithLength fd_with_length(fd, length);
  return named_fd_.insert(std::make_pair(name, std::move(fd_with_length)))
      .second;
}

// Called from Binder thread.
void JsSandboxIsolate::SetConsoleEnabled(
    JNIEnv* env,
    const base::android::JavaParamRef<jobject>& obj,
    const jboolean enable) {
  control_task_runner_->PostTask(
      FROM_HERE,
      base::BindOnce(&JsSandboxIsolate::SetConsoleEnabledOnControlThread,
                     base::Unretained(this), enable));
}

// Called from control sequence.
void JsSandboxIsolate::PostEvaluationToIsolateThread(
    const std::string code,
    scoped_refptr<JsSandboxIsolateCallback> callback) {
  cancelable_task_tracker_->PostTask(
      isolate_task_runner_.get(), FROM_HERE,
      base::BindOnce(&JsSandboxIsolate::EvaluateJavascriptOnThread,
                     base::Unretained(this), std::move(code),
                     std::move(callback)));
}

// Called from control sequence
void JsSandboxIsolate::PostFileDescriptorReadToIsolateThread(
    int fd,
    int64_t length,
    int64_t offset,
    base::android::ScopedJavaGlobalRef<jobject> pfd,
    scoped_refptr<JsSandboxIsolateCallback> callback) {
  cancelable_task_tracker_->PostTask(
      isolate_task_runner_.get(), FROM_HERE,
      base::BindOnce(&JsSandboxIsolate::ReadFileDescriptorOnThread,
                     base::Unretained(this), fd, length, offset, std::move(pfd),
                     std::move(callback)));
}

// Called from control sequence.
void JsSandboxIsolate::CreateCancelableTaskTracker() {
  cancelable_task_tracker_ = std::make_unique<base::CancelableTaskTracker>();
}

// Called from control sequence.
void JsSandboxIsolate::TerminateAndDestroy() {
  // This will cancel all pending executions.
  cancelable_task_tracker_.reset();
  isolate_holder_->isolate()->TerminateExecution();
  isolate_task_runner_->PostTask(
      FROM_HERE,
      base::BindOnce(&JsSandboxIsolate::DeleteSelf, base::Unretained(this)));
}

// Called from control sequence.
void JsSandboxIsolate::DestroyWhenPossible() {
  if (isolate_init_complete) {
    TerminateAndDestroy();
  } else {
    destroy_called_before_init = true;
  }
}

// Called from control sequence.
void JsSandboxIsolate::NotifyInitComplete() {
  if (destroy_called_before_init) {
    TerminateAndDestroy();
  }
  isolate_init_complete = true;
}

// Called from control sequence.
void JsSandboxIsolate::ConvertPromiseToArrayBufferInControlSequence(
    std::string name,
    std::unique_ptr<v8::Global<v8::ArrayBuffer>> array_buffer,
    std::unique_ptr<v8::Global<v8::Promise::Resolver>> resolver) {
  cancelable_task_tracker_->PostTask(
      isolate_task_runner_.get(), FROM_HERE,
      base::BindOnce(
          &JsSandboxIsolate::ConvertPromiseToArrayBufferInIsolateSequence,
          base::Unretained(this), std::move(name), std::move(array_buffer),
          std::move(resolver)));
}

// Called from control sequence.
//
// The array_buffer's API must only be used from the isolate thread.
void JsSandboxIsolate::ConvertPromiseToFailureInControlSequence(
    std::string name,
    std::unique_ptr<v8::Global<v8::ArrayBuffer>> array_buffer,
    std::unique_ptr<v8::Global<v8::Promise::Resolver>> resolver,
    std::string reason) {
  cancelable_task_tracker_->PostTask(
      isolate_task_runner_.get(), FROM_HERE,
      base::BindOnce(
          &JsSandboxIsolate::ConvertPromiseToFailureInIsolateSequence,
          base::Unretained(this), std::move(name), std::move(array_buffer),
          std::move(resolver), std::move(reason)));
}

// Called from Thread pool.
//
// The array_buffer's API must only be used from the isolate thread, but the
// internal data (inner_buffer) may be accessed in whatever thread is currently
// processing the task, so long as array_buffer remains alive.
void JsSandboxIsolate::ConvertPromiseToArrayBufferInThreadPool(
    base::ScopedFD fd,
    ssize_t length,
    std::string name,
    std::unique_ptr<v8::Global<v8::ArrayBuffer>> array_buffer,
    std::unique_ptr<v8::Global<v8::Promise::Resolver>> resolver,
    void* inner_buffer) {
  if (base::ReadFromFD(fd.get(),
                       base::make_span(static_cast<char*>(inner_buffer),
                                       base::checked_cast<size_t>(length)))) {
    control_task_runner_->PostTask(
        FROM_HERE,
        base::BindOnce(
            &JsSandboxIsolate::ConvertPromiseToArrayBufferInControlSequence,
            base::Unretained(this), std::move(name), std::move(array_buffer),
            std::move(resolver)));
  } else {
    std::string failure_reason = "Reading data failed.";
    control_task_runner_->PostTask(
        FROM_HERE,
        base::BindOnce(
            &JsSandboxIsolate::ConvertPromiseToFailureInControlSequence,
            base::Unretained(this), std::move(name), std::move(array_buffer),
            std::move(resolver), std::move(failure_reason)));
  }
}

// Called from isolate thread.
v8::Local<v8::ObjectTemplate> JsSandboxIsolate::CreateAndroidNamespaceTemplate(
    v8::Isolate* isolate) {
  v8::Local<v8::ObjectTemplate> android_namespace_template =
      v8::ObjectTemplate::New(isolate);
  v8::Local<v8::ObjectTemplate> consume_template =
      v8::ObjectTemplate::New(isolate);
  consume_template->Set(
      isolate, "consumeNamedDataAsArrayBuffer",
      gin::CreateFunctionTemplate(
          isolate,
          base::BindRepeating(&JsSandboxIsolate::ConsumeNamedDataAsArrayBuffer,
                              base::Unretained(this))));
  android_namespace_template->Set(isolate, "android", consume_template);
  return android_namespace_template;
}

// Called from isolate thread.
void JsSandboxIsolate::ReadFileDescriptorOnThread(
    int fd,
    int64_t length,
    int64_t offset,
    base::android::ScopedJavaGlobalRef<jobject> pfd,
    scoped_refptr<JsSandboxIsolateCallback> callback) {
  std::string code;

  if (lseek64(fd, offset, SEEK_SET) == -1) {
    if (errno != ESPIPE) {
      ReportFileDescriptorIOError(
          std::move(pfd), std::move(callback),
          base::StrCat({"Could not seek to offset: ", strerror(errno)}));
      return;
    } else {
      // Just read these bytes and discard
      int64_t bytes_read = ReadBytesFromFdAndDiscard(fd, offset);
      if (bytes_read == -1) {
        ReportFileDescriptorIOError(
            std::move(pfd), std::move(callback),
            base::StrCat({"Could not skip to offset: ", strerror(errno)}));
        return;
      } else if (bytes_read != offset) {
        ReportFileDescriptorIOError(
            std::move(pfd), std::move(callback),
            base::StrCat({"Short read, could only read ",
                          base::NumberToString(bytes_read), " bytes"}));
        return;
      }
    }
  }

  if (length >= 0) {
    code.resize(length);
    if (!base::ReadFromFD(fd, code)) {
      ReportFileDescriptorIOError(std::move(pfd), std::move(callback),
                                  "Failed to read data from file descriptor");
      return;
    }
  } else if (length == kUnknownAssetFileDescriptorLength) {
    if (!ReadFdToStringTillEof(fd, code)) {
      ReportFileDescriptorIOError(
          std::move(pfd), std::move(callback),
          base::StrCat({"Failed to read data till EOF: ", strerror(errno)}));
      return;
    }
  }

  JNIEnv* env = base::android::AttachCurrentThread();

  // check for error on the client side irrespective of errorCode
  base::android::ScopedJavaLocalRef<jstring> error =
      android_webview::Java_JsSandboxIsolate_checkStreamingErrorAndClosePfd(
          env, pfd);

  if (error) {
    callback->ReportFileDescriptorIOFailedError(
        base::StrCat({"Failed to read data from file descriptor: ",
                      ConvertJavaStringToUTF8(env, error)}));
    return;
  }

  // no error reported, proceed for evaluation
  EvaluateJavascriptOnThread(std::move(code), std::move(callback));
}

void JsSandboxIsolate::ReportFileDescriptorIOError(
    base::android::ScopedJavaGlobalRef<jobject> pfd,
    scoped_refptr<JsSandboxIsolateCallback> callback,
    std::string errorMessage) {
  JNIEnv* env = base::android::AttachCurrentThread();

  // check for error on the client side irrespective of errorCode
  base::android::ScopedJavaLocalRef<jstring> error =
      android_webview::Java_JsSandboxIsolate_checkStreamingErrorAndClosePfd(
          env, pfd);

  if (error) {
    errorMessage += base::StrCat(
        {"; Application sent error: ", ConvertJavaStringToUTF8(env, error)});
  }

  callback->ReportFileDescriptorIOFailedError(errorMessage);
}

// Called from isolate thread.
//
// Note that this will never be called if the isolate has "crashed" due to OOM
// and frozen its isolate thread.
void JsSandboxIsolate::DeleteSelf() {
  delete this;
}

// Called from isolate thread.
void JsSandboxIsolate::InitializeIsolateOnThread() {
  std::unique_ptr<v8::Isolate::CreateParams> params =
      gin::IsolateHolder::getDefaultIsolateParams();
  params->array_buffer_allocator = array_buffer_allocator_.get();
  if (isolate_max_heap_size_bytes_ > 0) {
    params->constraints.ConfigureDefaultsFromHeapSize(
        0, AdjustToValidHeapSize(isolate_max_heap_size_bytes_));
  }
  isolate_holder_ = std::make_unique<gin::IsolateHolder>(
      base::SingleThreadTaskRunner::GetCurrentDefault(),
      gin::IsolateHolder::AccessMode::kSingleThread,
      gin::IsolateHolder::IsolateType::kUtility, std::move(params));
  v8::Isolate* isolate = isolate_holder_->isolate();
  isolate_scope_ = std::make_unique<v8::Isolate::Scope>(isolate);
  isolate->SetMicrotasksPolicy(v8::MicrotasksPolicy::kAuto);

  isolate->AddNearHeapLimitCallback(&JsSandboxIsolate::NearHeapLimitCallback,
                                    this);
  v8::HandleScope handle_scope(isolate);

  v8::Local<v8::ObjectTemplate> android_template =
      CreateAndroidNamespaceTemplate(isolate);
  v8::Local<v8::Context> context =
      v8::Context::New(isolate, nullptr, android_template);

  context_holder_ = std::make_unique<gin::ContextHolder>(isolate);
  context_holder_->SetContext(context);

  control_task_runner_->PostTask(
      FROM_HERE, base::BindOnce(&JsSandboxIsolate::NotifyInitComplete,
                                base::Unretained(this)));
}

// Called from isolate thread.
void JsSandboxIsolate::EvaluateJavascriptOnThread(
    const std::string code,
    scoped_refptr<JsSandboxIsolateCallback> callback) {
  ongoing_evaluation_callbacks_.emplace(callback);

  v8::HandleScope handle_scope(isolate_holder_->isolate());
  v8::Context::Scope scope(context_holder_->context());
  v8::Isolate* v8_isolate = isolate_holder_->isolate();
  v8::TryCatch try_catch(v8_isolate);

  // Compile
  v8::ScriptOrigin origin(gin::StringToV8(v8_isolate, resource_name));
  v8::MaybeLocal<v8::Script> maybe_script = v8::Script::Compile(
      context_holder_->context(), gin::StringToV8(v8_isolate, code), &origin);
  std::string compile_error = "";
  if (try_catch.HasCaught()) {
    compile_error = GetStackTrace(try_catch, v8_isolate);
  }
  v8::Local<v8::Script> script;
  if (!maybe_script.ToLocal(&script)) {
    UseCallback(callback)->ReportError(
        JsSandboxIsolateCallback::ErrorType::kJsEvaluationError, compile_error);
    return;
  }

  // Run
  v8::MaybeLocal<v8::Value> maybe_result =
      script->Run(context_holder_->context());
  std::string run_error = "";
  if (try_catch.HasTerminated()) {
    // Client side will take care of it for now.
    return;
  } else if (try_catch.HasCaught()) {
    run_error = GetStackTrace(try_catch, v8_isolate);
  }
  v8::Local<v8::Value> value;
  if (maybe_result.ToLocal(&value)) {
    if (value->IsPromise()) {
      v8::Local<v8::Promise> promise = value.As<v8::Promise>();
      // If the promise is already completed, retrieve and handle the result
      // directly.
      if (promise->State() == v8::Promise::PromiseState::kFulfilled) {
        std::string result = gin::V8ToString(v8_isolate, promise->Result());
        UseCallback(callback)->ReportResult(result);
        return;
      }
      if (promise->State() == v8::Promise::PromiseState::kRejected) {
        v8::Local<v8::Message> message = v8::Exception::CreateMessage(
            isolate_holder_->isolate(), promise->Result());
        std::string error_message = GetStackTrace(message, v8_isolate);
        UseCallback(callback)->ReportError(
            JsSandboxIsolateCallback::ErrorType::kJsEvaluationError,
            error_message);
        return;
      }
      v8::Local<v8::Function> fulfill_fun =
          gin::CreateFunctionTemplate(
              v8_isolate,
              base::BindRepeating(&JsSandboxIsolate::PromiseFulfillCallback,
                                  base::Unretained(this),
                                  base::RetainedRef(callback)))
              ->GetFunction(context_holder_->context())
              .ToLocalChecked();
      v8::Local<v8::Function> reject_fun =
          gin::CreateFunctionTemplate(
              v8_isolate,
              base::BindRepeating(&JsSandboxIsolate::PromiseRejectCallback,
                                  base::Unretained(this),
                                  base::RetainedRef(callback)))
              ->GetFunction(context_holder_->context())
              .ToLocalChecked();

      promise->Then(context_holder_->context(), fulfill_fun, reject_fun)
          .ToLocalChecked();
    } else {
      std::string result = gin::V8ToString(v8_isolate, value);
      UseCallback(callback)->ReportResult(result);
    }
  } else {
    UseCallback(callback)->ReportError(
        JsSandboxIsolateCallback::ErrorType::kJsEvaluationError, run_error);
  }
}

void JsSandboxIsolate::PromiseFulfillCallback(
    scoped_refptr<JsSandboxIsolateCallback> callback,
    gin::Arguments* args) {
  std::string result;
  args->GetNext(&result);
  UseCallback(callback)->ReportResult(result);
}

void JsSandboxIsolate::PromiseRejectCallback(
    scoped_refptr<JsSandboxIsolateCallback> callback,
    gin::Arguments* args) {
  v8::HandleScope handle_scope(isolate_holder_->isolate());
  v8::Local<v8::Value> value;
  args->GetNext(&value);
  v8::Local<v8::Message> message =
      v8::Exception::CreateMessage(isolate_holder_->isolate(), value);
  std::string error_message =
      GetStackTrace(message, isolate_holder_->isolate());
  UseCallback(callback)->ReportError(
      JsSandboxIsolateCallback::ErrorType::kJsEvaluationError, error_message);
}

// Called from isolate thread.
void JsSandboxIsolate::ConvertPromiseToArrayBufferInIsolateSequence(
    std::string name,
    std::unique_ptr<v8::Global<v8::ArrayBuffer>> array_buffer,
    std::unique_ptr<v8::Global<v8::Promise::Resolver>> resolver) {
  v8::HandleScope handle_scope(isolate_holder_->isolate());
  v8::Context::Scope scope(context_holder_->context());

  resolver->Get(isolate_holder_->isolate())
      ->Resolve(context_holder_->context(),
                array_buffer->Get(isolate_holder_->isolate()))
      .ToChecked();
}

// Called from isolate thread.
//
// We pass the array_buffer to the isolate thread so that it (or the handle)
// only gets destructed from the isolate thread.
void JsSandboxIsolate::ConvertPromiseToFailureInIsolateSequence(
    std::string name,
    std::unique_ptr<v8::Global<v8::ArrayBuffer>> array_buffer,
    std::unique_ptr<v8::Global<v8::Promise::Resolver>> resolver,
    std::string reason) {
  v8::HandleScope handle_scope(isolate_holder_->isolate());
  v8::Context::Scope scope(context_holder_->context());

  // Allow array buffer to be garbage collectable before further V8 calls.
  array_buffer = nullptr;

  resolver->Get(isolate_holder_->isolate())
      ->Reject(context_holder_->context(),
               v8::Exception::Error(
                   gin::StringToV8(isolate_holder_->isolate(), reason)))
      .ToChecked();
}

// Called from isolate thread.
void JsSandboxIsolate::ConsumeNamedDataAsArrayBuffer(gin::Arguments* args) {
  v8::Isolate* isolate = args->isolate();
  v8::Global<v8::Promise::Resolver> global_resolver(
      isolate, v8::Promise::Resolver::New(isolate->GetCurrentContext())
                   .ToLocalChecked());
  v8::Local<v8::Promise> promise = global_resolver.Get(isolate)->GetPromise();
  if (args->Length() != 1) {
    std::string reason = "Unexpected number of arguments";
    global_resolver.Get(isolate_holder_->isolate())
        ->Reject(context_holder_->context(),
                 v8::Exception::Error(
                     gin::StringToV8(isolate_holder_->isolate(), reason)))
        .ToChecked();
    args->Return(promise);
    return;
  }
  std::string name;
  args->GetNext(&name);
  base::ScopedFD fd;
  ssize_t length;
  {
    base::AutoLock lock(named_fd_lock_);
    auto entry = named_fd_.find(name);
    if (entry != named_fd_.end()) {
      // When we move the fd, we invalidate the entry in the map such that it
      // cannot be used again, even if the operation fails before we read any
      // data from the pipe.
      fd = std::move(entry->second.fd);
      length = entry->second.length;
    }
  }
  if (!fd.is_valid()) {
    std::string reason = "No NamedData available with the given name";
    global_resolver.Get(isolate_holder_->isolate())
        ->Reject(context_holder_->context(),
                 v8::Exception::Error(
                     gin::StringToV8(isolate_holder_->isolate(), reason)))
        .ToChecked();
    args->Return(promise);
    return;
  }

  // V8 only accounts for the external memory used by backing stores once they
  // are bound to an array buffer. So we set up the whole array buffer up-front
  // on the isolate thread. (This will prevent V8's view of external memory
  // usage getting out of sync with our own.)
  v8::MaybeLocal<v8::ArrayBuffer> maybe_array_buffer =
      tryAllocateArrayBuffer(length);
  if (maybe_array_buffer.IsEmpty()) {
    const std::string reason =
        "Array buffer allocation failed for consumeNamedDataAsArrayBuffer";
    global_resolver.Get(isolate_holder_->isolate())
        ->Reject(context_holder_->context(),
                 v8::Exception::RangeError(
                     gin::StringToV8(isolate_holder_->isolate(), reason)))
        .ToChecked();
    args->Return(promise);
    return;
  }

  v8::Local<v8::ArrayBuffer> local_array_buffer =
      maybe_array_buffer.ToLocalChecked();
  void* const inner_buffer = local_array_buffer->Data();
  // V8 documentation provides no guarantees about the thread-safety of Globals
  // - even move construction/destruction. Wrap it in a unique_ptr so that it
  // can be treated as an opaque pointer until it's handed back to the isolate
  // thread.
  std::unique_ptr<v8::Global<v8::ArrayBuffer>> global_array_buffer(
      std::make_unique<v8::Global<v8::ArrayBuffer>>(
          isolate, std::move(local_array_buffer)));
  base::ThreadPool::PostTask(
      FROM_HERE, {base::MayBlock()},
      base::BindOnce(&JsSandboxIsolate::ConvertPromiseToArrayBufferInThreadPool,
                     base::Unretained(this), std::move(fd), length,
                     std::move(name), std::move(global_array_buffer),
                     std::make_unique<v8::Global<v8::Promise::Resolver>>(
                         std::move(global_resolver)),
                     inner_buffer));
  args->Return(promise);
}

// Called from isolate thread.
[[noreturn]] size_t JsSandboxIsolate::NearHeapLimitCallback(
    void* data,
    size_t /*current_heap_limit*/,
    size_t /*initial_heap_limit*/) {
  android_webview::JsSandboxIsolate* js_sandbox_isolate =
      static_cast<android_webview::JsSandboxIsolate*>(data);
  js_sandbox_isolate->MemoryLimitExceeded();
}

// Called from isolate thread.
[[noreturn]] void JsSandboxIsolate::MemoryLimitExceeded() {
  ReportOutOfMemory();
  FreezeThread();
}

// Called from isolate thread
void JsSandboxIsolate::ReportOutOfMemory() {
  LOG(ERROR) << "Isolate has OOMed";

  const uint64_t memory_limit = uint64_t{isolate_max_heap_size_bytes_};
  v8::HeapStatistics heap_statistics;
  isolate_holder_->isolate()->GetHeapStatistics(&heap_statistics);
  const uint64_t v8_heap_usage = heap_statistics.used_heap_size();
  // Note that we use our own memory accounting, and not V8's external memory
  // accounting, for non-heap usage. These numbers can differ, particularly as
  // our own memory accounting considers whole pages rather than just bytes.
  const uint64_t non_v8_heap_usage =
      uint64_t{array_buffer_allocator_->GetUsage()};

  std::ostringstream details;
  details << "Memory limit exceeded.\n";
  if (memory_limit > 0) {
    details << "Memory limit: " << memory_limit << " bytes\n";
  } else {
    details << "Memory limit not explicitly configured\n";
  }
  details << "V8 heap usage: " << v8_heap_usage << " bytes\n";
  details << "Non-V8 heap usage: " << non_v8_heap_usage << " bytes\n";
  const std::string details_str = details.str();

  JNIEnv* const env = base::android::AttachCurrentThread();

  const bool client_got_termination =
      android_webview::Java_JsSandboxIsolate_sendTermination(
          env, j_isolate_,
          static_cast<jint>(TerminationStatus::kMemoryLimitExceeded),
          base::android::ConvertUTF8ToJavaString(env, details_str));
  if (client_got_termination) {
    // Don't send any evaluation errors - the client will deal with them itself.
    return;
  }

  bool client_notified_via_evaluation = false;
  if (ongoing_evaluation_callbacks_.size() > 0) {
    // It is safe to erase items from a std::set while iterating through it.
    auto callback_it = ongoing_evaluation_callbacks_.begin();
    while (callback_it != ongoing_evaluation_callbacks_.end()) {
      UseCallback(*callback_it)
          ->ReportError(
              JsSandboxIsolateCallback::ErrorType::kMemoryLimitExceeded,
              details_str);
      callback_it++;
    }
    client_notified_via_evaluation = true;
  }

  // Some pre-stable clients do not support termination notifications and only
  // support signaling OOMs via evaluation callbacks. Ensure the client has been
  // notified through at least one mechanism.
  CHECK(client_notified_via_evaluation)
      << "Isolate ran out of memory but the client does not support "
      << "termination notifications and there are no ongoing evaluations "
      << "through which to signal an error.";
}

// Halt thread until process dies.
[[noreturn]] void JsSandboxIsolate::FreezeThread() {
  // There is no well-defined way to fully terminate a thread prematurely, so we
  // idle the thread forever.
  //
  // TODO(ashleynewson): In future, we may want to look into ways to cleanup or
  // even properly terminate the thread if language or V8 features allow for it,
  // as we currently hold onto (essentially leaking) all resources this isolate
  // has accumulated up to this point. C++20's <stop_token> (not permitted in
  // Chromium at time of writing) may contribute to such a future solution.

  base::ScopedAllowBaseSyncPrimitives allow_base_sync_primitives;
  base::WaitableEvent().Wait();
  // Unreachable. Make sure the compiler understands that.
  base::ImmediateCrash();
}

// Called from isolate thread.
//
// Attempts to allocate an array buffer of given size. If unsuccessful, no array
// is returned, instead of an OOM crash.
//
// The public V8 APIs don't expose native methods for trying to allocate an
// array buffer without the risk of an OOM crash.
//
// The returned buffer will not be resizable.
v8::MaybeLocal<v8::ArrayBuffer> JsSandboxIsolate::tryAllocateArrayBuffer(
    const size_t length) {
  void* buffer = array_buffer_allocator_->Allocate(length);
  if (!buffer) {
    // Encourage V8 to perform some garbage collection, which might result in
    // previous array buffers getting deallocated. Note this won't free memory
    // from the heap itself, but it will clean up any garbage which is keeping
    // otherwise disused array buffers alive.
    //
    // Note that this may cause overly aggressive garbage collection, but is the
    // only sensible API provided.
    isolate_holder_->isolate()->LowMemoryNotification();
    // Try again after GC.
    buffer = array_buffer_allocator_->Allocate(length);
    if (!buffer) {
      return v8::MaybeLocal<v8::ArrayBuffer>();
    }
  }

  std::unique_ptr<v8::BackingStore> backing_store =
      v8::ArrayBuffer::NewBackingStore(
          buffer, length,
          [](void* buffer_to_delete, size_t length, void* allocator) {
            static_cast<v8::ArrayBuffer::Allocator*>(allocator)->Free(
                buffer_to_delete, length);
          },
          array_buffer_allocator_.get());

  // We do not need to call AdjustAmountOfExternalAllocatedMemory ourselves. V8
  // will automatically call AdjustAmountOfExternalAllocatedMemory with the size
  // of the backing store involved, which may further trigger garbage
  // collections if memory usage is being unreasonable. This is done deep within
  // the call to v8::ArrayBuffer::New().
  return v8::MaybeLocal<v8::ArrayBuffer>(v8::ArrayBuffer::New(
      isolate_holder_->isolate(), std::move(backing_store)));
}

// Called from isolate thread.
void JsSandboxIsolate::EnableOrDisableInspectorAsNeeded() {
  const bool needed = console_enabled_;
  const bool already_enabled = bool{inspector_client_};

  if (already_enabled && !needed) {
    inspector_session_.reset();
    inspector_channel_.reset();
    inspector_.reset();
    inspector_client_.reset();
  } else if (!already_enabled && needed) {
    v8::HandleScope handle_scope(isolate_holder_->isolate());
    v8::Context::Scope scope(context_holder_->context());

    constexpr int context_group_id = 1;
    inspector_client_ = std::make_unique<InspectorClient>(*this);
    inspector_ = v8_inspector::V8Inspector::create(isolate_holder_->isolate(),
                                                   inspector_client_.get());
    inspector_channel_ =
        static_cast<std::unique_ptr<v8_inspector::V8Inspector::Channel>>(
            std::make_unique<NoopInspectorChannel>());
    inspector_session_ =
        inspector_->connect(context_group_id, inspector_channel_.get(),
                            /*state=*/v8_inspector::StringView(),
                            v8_inspector::V8Inspector::kFullyTrusted,
                            v8_inspector::V8Inspector::kNotWaitingForDebugger);
    inspector_->contextCreated(v8_inspector::V8ContextInfo(
        context_holder_->context(), context_group_id,
        /*humanReadableName=*/v8_inspector::StringView()));
  }
}

// Called from control sequence.
void JsSandboxIsolate::SetConsoleEnabledOnControlThread(const bool enable) {
  cancelable_task_tracker_->PostTask(
      isolate_task_runner_.get(), FROM_HERE,
      base::BindOnce(&JsSandboxIsolate::SetConsoleEnabledOnIsolateThread,
                     base::Unretained(this), enable));
}

// Called from isolate thread.
void JsSandboxIsolate::SetConsoleEnabledOnIsolateThread(const bool enable) {
  console_enabled_ = enable;
  EnableOrDisableInspectorAsNeeded();
}

const scoped_refptr<JsSandboxIsolateCallback>& JsSandboxIsolate::UseCallback(
    const scoped_refptr<JsSandboxIsolateCallback>& callback) {
  const size_t removed = ongoing_evaluation_callbacks_.erase(callback);
  CHECK_EQ(removed, size_t{1});
  return callback;
}

static void JNI_JsSandboxIsolate_InitializeEnvironment(JNIEnv* env) {
  base::ThreadPoolInstance::CreateAndStartWithDefaultParams("JsSandboxIsolate");
#ifdef V8_USE_EXTERNAL_STARTUP_DATA
  gin::V8Initializer::LoadV8Snapshot();
#endif
  gin::IsolateHolder::Initialize(gin::IsolateHolder::kStrictMode,
                                 gin::ArrayBufferAllocator::SharedInstance());
}

static jlong JNI_JsSandboxIsolate_CreateNativeJsSandboxIsolateWrapper(
    JNIEnv* env,
    const base::android::JavaParamRef<jobject>& j_sandbox_isolate,
    jlong max_heap_size_bytes) {
  CHECK_GE(max_heap_size_bytes, 0);
  JsSandboxIsolate* processor = new JsSandboxIsolate(
      j_sandbox_isolate, base::saturated_cast<size_t>(max_heap_size_bytes));
  return reinterpret_cast<intptr_t>(processor);
}

}  // namespace android_webview