// Copyright 2015 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

// This is a part of the Android-specific Chromium dynamic linker.
//
// See linker_jni.h for more details and the dependency rules.

#include "base/android/linker/linker_jni.h"

#include <errno.h>
#include <fcntl.h>
#include <inttypes.h>
#include <jni.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <unistd.h>

#include <memory>

namespace chromium_android_linker {

// Variable containing LibInfo for the loaded library.
LibInfo_class s_lib_info_fields;

String::String(JNIEnv* env, jstring str) {
  size_ = env->GetStringUTFLength(str);
  ptr_ = static_cast<char*>(::malloc(size_ + 1));

  // Note: This runs before browser native code is loaded, and so cannot
  // rely on anything from base/. This means that we must use
  // GetStringUTFChars() and not base::android::ConvertJavaStringToUTF8().
  //
  // GetStringUTFChars() suffices because the only strings used here are
  // paths to APK files or names of shared libraries, all of which are
  // plain ASCII, defined and hard-coded by the Chromium Android build.
  //
  // For more: see
  //   https://crbug.com/508876
  //
  // Note: GetStringUTFChars() returns Java UTF-8 bytes. This is good
  // enough for the linker though.
  const char* bytes = env->GetStringUTFChars(str, nullptr);
  ::memcpy(ptr_, bytes, size_);
  ptr_[size_] = '\0';

  env->ReleaseStringUTFChars(str, bytes);
}

bool IsValidAddress(jlong address) {
  bool result = static_cast<jlong>(static_cast<uintptr_t>(address)) == address;
  if (!result) {
    LOG_ERROR("Invalid address 0x%" PRIx64, static_cast<uint64_t>(address));
  }
  return result;
}

// Finds the jclass JNI reference corresponding to a given |class_name|.
// |env| is the current JNI environment handle.
// On success, return true and set |*clazz|.
bool InitClassReference(JNIEnv* env, const char* class_name, jclass* clazz) {
  *clazz = env->FindClass(class_name);
  if (!*clazz) {
    LOG_ERROR("Could not find class for %s", class_name);
    return false;
  }
  return true;
}

// Initializes a jfieldID corresponding to the field of a given |clazz|,
// with name |field_name| and signature |field_sig|.
// |env| is the current JNI environment handle.
// On success, return true and set |*field_id|.
bool InitFieldId(JNIEnv* env,
                 jclass clazz,
                 const char* field_name,
                 const char* field_sig,
                 jfieldID* field_id) {
  *field_id = env->GetFieldID(clazz, field_name, field_sig);
  if (!*field_id) {
    LOG_ERROR("Could not find ID for field '%s'", field_name);
    return false;
  }
  LOG_INFO("Found ID %p for field '%s'", *field_id, field_name);
  return true;
}

namespace {

// With mmap(2) reserves a range of virtual addresses.
//
// The range must start with |hint| and be of size |size|. The |hint==0|
// indicates that the address of the mapping should be chosen at random,
// utilizing ASLR built into mmap(2).
//
// The start of the resulting region is returned in |address|.
//
// The value 0 returned iff the attempt failed (a part of the address range is
// already reserved by some other subsystem).
void ReserveAddressWithHint(uintptr_t hint, uintptr_t* address, size_t* size) {
  void* ptr = reinterpret_cast<void*>(hint);
  void* new_ptr = mmap(ptr, kAddressSpaceReservationSize, PROT_NONE,
                       MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
  if (new_ptr == MAP_FAILED) {
    PLOG_ERROR("mmap");
    *address = 0;
  } else if ((hint != 0) && (new_ptr != ptr)) {
    // Something grabbed the address range before the early phase of the
    // linker had a chance, this should be uncommon.
    LOG_ERROR("Address range starting at 0x%" PRIxPTR " was not free to use",
              hint);
    munmap(new_ptr, kAddressSpaceReservationSize);
    *address = 0;
  } else {
    *address = reinterpret_cast<uintptr_t>(new_ptr);
    *size = kAddressSpaceReservationSize;
    LOG_INFO("Reserved region at address: 0x%" PRIxPTR ", size: 0x%zu",
             *address, *size);
  }
}

bool ScanRegionInBuffer(const char* buf,
                        size_t length,
                        uintptr_t* out_address,
                        size_t* out_size) {
  const char* position = strstr(buf, "[anon:libwebview reservation]");
  if (!position)
    return false;

  const char* line_start = position;
  while (line_start > buf) {
    line_start--;
    if (*line_start == '\n') {
      line_start++;
      break;
    }
  }

  // Extract the region start and end. The failures below should not happen as
  // long as the reservation is made the same way in
  // frameworks/base/native/webview/loader/loader.cpp.
  uintptr_t vma_start, vma_end;
  char permissions[5] = {'\0'};  // Ensure a null-terminated string.
  // Example line from proc(5):
  // address           perms offset  dev   inode   pathname
  // 00400000-00452000 r-xp 00000000 08:02 173521  /usr/bin/dbus-daemon
  if (sscanf(line_start, "%" SCNxPTR "-%" SCNxPTR " %4c", &vma_start, &vma_end,
             permissions) < 3) {
    return false;
  }

  if (strcmp(permissions, "---p"))
    return false;

  if (vma_start % PAGE_SIZE || vma_end % PAGE_SIZE)
    return false;

  *out_address = static_cast<uintptr_t>(vma_start);
  *out_size = vma_end - vma_start;

  return true;
}

bool FindRegionInOpenFile(int fd, uintptr_t* out_address, size_t* out_size) {
  constexpr size_t kMaxLineLength = 256;
  constexpr size_t kReadSize = PAGE_SIZE;

  // Loop until no bytes left to scan. On every iteration except the last, fill
  // the buffer till the end. On every iteration except the first, the buffer
  // begins with kMaxLineLength bytes from the end of the previous fill.
  char buf[kReadSize + kMaxLineLength + 1];
  buf[kReadSize + kMaxLineLength] = '\0';  // Stop strstr().
  size_t pos = 0;
  size_t bytes_requested = kReadSize + kMaxLineLength;
  bool reached_end = false;
  while (true) {
    // Fill the |buf| to the maximum and determine whether reading reached the
    // end.
    size_t bytes_read = 0;
    do {
      ssize_t rv = HANDLE_EINTR(
          read(fd, buf + pos + bytes_read, bytes_requested - bytes_read));
      if (rv == 0) {
        reached_end = true;
      } else if (rv < 0) {
        PLOG_ERROR("read to find webview reservation");
        return false;
      }
      bytes_read += rv;
    } while (!reached_end && (bytes_read < bytes_requested));

    // Return results if the buffer contains the pattern.
    if (ScanRegionInBuffer(buf, pos + bytes_read, out_address, out_size))
      return true;

    // Did not find the pattern.
    if (reached_end)
      return false;

    // The buffer is filled to the end. Copy the end bytes to the beginning,
    // allowing to scan these bytes on the next iteration.
    memcpy(buf, buf + kReadSize, kMaxLineLength);
    pos = kMaxLineLength;
    bytes_requested = kReadSize;
  }
}

}  // namespace

bool FindWebViewReservation(uintptr_t* out_address, size_t* out_size) {
  // Note: reading /proc/PID/maps or /proc/PID/smaps is inherently racy. Among
  // other things, the kernel provides these guarantees:
  // * Each region record (line) is well formed
  // * If there is something at a given vaddr during the entirety of the life of
  //   the smaps/maps walk, there will be some output for it.
  //
  // In order for the address/size extraction to be safe, these precausions are
  // made in base/android/linker:
  // * Modification of the range is done only after this function exits
  // * The use of the range is avoided if it is not sufficient in size, which
  //   might happen if it gets split
  const char kFileName[] = "/proc/self/maps";
  int fd = HANDLE_EINTR(open(kFileName, O_RDONLY));
  if (fd == -1) {
    PLOG_ERROR("open %s", kFileName);
    return false;
  }

  bool result = FindRegionInOpenFile(fd, out_address, out_size);
  close(fd);
  return result;
}

// Performs as described in Linker.java.
JNI_GENERATOR_EXPORT void
Java_org_chromium_base_library_1loader_LinkerJni_nativeFindMemoryRegionAtRandomAddress(
    JNIEnv* env,
    jclass clazz,
    jobject lib_info_obj,
    jboolean keep_reserved) {
  LOG_INFO("Entering");
  uintptr_t address;
  size_t size;
  ReserveAddressWithHint(0, &address, &size);
  if (!keep_reserved && address != 0) {
    munmap(reinterpret_cast<void*>(address), size);
  }
  s_lib_info_fields.SetLoadInfo(env, lib_info_obj, address, size);
}

// Performs as described in Linker.java.
JNI_GENERATOR_EXPORT void
Java_org_chromium_base_library_1loader_LinkerJni_nativeReserveMemoryForLibrary(
    JNIEnv* env,
    jclass clazz,
    jobject lib_info_obj) {
  LOG_INFO("Entering");
  uintptr_t address;
  size_t size;
  s_lib_info_fields.GetLoadInfo(env, lib_info_obj, &address, &size);
  ReserveAddressWithHint(address, &address, &size);
  s_lib_info_fields.SetLoadInfo(env, lib_info_obj, address, size);
}

// Performs as described in Linker.java.
JNI_GENERATOR_EXPORT jboolean
Java_org_chromium_base_library_1loader_LinkerJni_nativeFindRegionReservedByWebViewZygote(
    JNIEnv* env,
    jclass clazz,
    jobject lib_info_obj) {
  LOG_INFO("Entering");
  uintptr_t address;
  size_t size;
  if (!FindWebViewReservation(&address, &size))
    return false;
  s_lib_info_fields.SetLoadInfo(env, lib_info_obj, address, size);
  return true;
}

}  // namespace chromium_android_linker
