/*
 * Copyright (C) 2008 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.
 */

#include <cutils/ashmem.h>

#define LOG_TAG "ashmem"

#include <errno.h>
#include <fcntl.h>
#include <linux/ashmem.h>
#include <linux/memfd.h>
#include <log/log.h>
#include <sys/ioctl.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <sys/syscall.h>
#include <sys/sysmacros.h>
#include <sys/types.h>
#include <unistd.h>

#include <android-base/file.h>
#include <android-base/properties.h>
#include <android-base/strings.h>
#include <android-base/stringprintf.h>
#include <android-base/unique_fd.h>

#include "ashmem-internal.h"

#include <atomic>

/*
 * Implementation of the userspace ashmem API for devices.
 *
 * This may use ashmem or memfd. See use_memfd().
 *
 * See ashmem-host.cpp for the temporary file based alternative for the host.
 */

/* ashmem identity */
static std::atomic<dev_t> __ashmem_rdev;

/* set to true for verbose logging and other debug  */
static bool debug_log = false;

static bool __use_memfd() {
    // Used to force enable memfd usage. In the future this property will be removed once we switch
    // everything over to memfd.
    if (android::base::GetBoolProperty("sys.use_memfd", false)) {
        if (debug_log) {
            ALOGD("sys.use_memfd=true so using memfd");
        }
        return true;
    }

    // Ensure that the kernel supports the memfd_class policy capability.
    if (access("/sys/fs/selinux/policy_capabilities/memfd_class", F_OK) != 0) {
        if (debug_log) {
            ALOGD("Not using memfd: kernel does not support memfd_class sepolicy capability");
        }
        return false;
    }

    // Per VSR-3.5.2-004, the memfd_class policy capability is required for devices with vendor API
    // level 202604+, so use memfd on those devices.
    const int min_vendor_api_level = 202604;
    const int vendor_api_level = android::base::GetIntProperty("ro.vendor.api_level", -1);
    if (vendor_api_level < min_vendor_api_level) {
        if (debug_log) {
            ALOGD("Not using memfd: device vendor API level %d < %d the minimum vendor API level required for memfd",
                  vendor_api_level, min_vendor_api_level);
        }
        return false;
    }

    // Make the minimum target SDK version match vendor API level 202604 to avoid breaking
    // assumptions that older applications might make about the fd they allocate.
    const int min_app_target_sdk_version = 37;
    const int app_target_sdk_version = android_get_application_target_sdk_version();
    if (app_target_sdk_version < min_app_target_sdk_version) {
        if (debug_log) {
            ALOGD("Not using memfd: application target SDK version %d < %d the minimum target SDK version required for memfd",
                  app_target_sdk_version, min_app_target_sdk_version);
        }
        return false;
    }

    if (debug_log) {
        ALOGD("memfd requirements satisfied: memfd_class capability supported, device vendor API level: %d, and application target SDK version: %d",
              vendor_api_level, app_target_sdk_version);
    }

    return true;
}

bool use_memfd() {
    static bool use_memfd = __use_memfd();
    return use_memfd;
}

static std::string get_ashmem_device_path() {
    static const std::string boot_id_path = "/proc/sys/kernel/random/boot_id";
    std::string boot_id;
    if (!android::base::ReadFileToString(boot_id_path, &boot_id)) {
        ALOGE("Failed to read %s: %m", boot_id_path.c_str());
        return "";
    }
    boot_id = android::base::Trim(boot_id);

    return "/dev/ashmem" + boot_id;
}

static bool __init_ashmem_rdev() {
    const std::string ashmem_device_path = get_ashmem_device_path();
    if (ashmem_device_path.empty()) {
        return false;
    }

    struct stat st;
    if (TEMP_FAILURE_RETRY(stat(ashmem_device_path.c_str(), &st)) == -1) {
        ALOGE("Unable to stat ashmem device: %m");
        return false;
    }
    if (!S_ISCHR(st.st_mode)) {
        ALOGE("ashmem device is not a character device");
        errno = ENOTTY;
        return false;
    }

    __ashmem_rdev = st.st_rdev;
    return true;
}

int __ashmem_open() {
    static const std::string ashmem_device_path = get_ashmem_device_path();

    if (ashmem_device_path.empty()) {
        return -1;
    }

    if (__ashmem_rdev == 0 && !__init_ashmem_rdev()) {
        return -1;
    }

    android::base::unique_fd fd(TEMP_FAILURE_RETRY(open(ashmem_device_path.c_str(), O_RDWR | O_CLOEXEC)));
    if (!fd.ok()) {
        ALOGE("Unable to open ashmem device: %m");
        return -1;
    }

    return fd.release();
}

/* Make sure file descriptor references ashmem, negative number means false */
// TODO: return bool
static int __ashmem_is_ashmem(int fd, bool fatal) {
    if (__ashmem_rdev == 0 && !__init_ashmem_rdev()) {
        return -1;
    }

    struct stat st;
    if (fstat(fd, &st) == -1) return -1;
    if (S_ISCHR(st.st_mode) && st.st_rdev == __ashmem_rdev) {
      return 0;
    }

    // TODO: move this to the single caller that actually wants it
    if (fatal) {
        LOG_ALWAYS_FATAL("illegal fd=%d mode=0%o rdev=%d:%d expected 0%o %d:%d",
            fd, st.st_mode, major(st.st_rdev), minor(st.st_rdev),
            S_IFCHR | S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH | S_IRGRP,
            major(__ashmem_rdev), minor(__ashmem_rdev));
        /* NOTREACHED */
    }

    errno = ENOTTY;
    return -1;
}

static int __ashmem_check_failure(int fd, int result) {
    if (result == -1 && errno == ENOTTY) __ashmem_is_ashmem(fd, true);
    return result;
}

static bool is_memfd_fd(int fd) {
    std::string fd_path = android::base::StringPrintf("/proc/self/fd/%d", fd);
    std::string result;
    if (!android::base::Readlink(fd_path, &result)) {
        ALOGE("readlink(%s) failed: %m", fd_path.c_str());
        return false;
    }
    return result.starts_with("/memfd:");
}

int ashmem_valid(int fd) {
    if (is_memfd_fd(fd)) {
        return 1;
    }

    return __ashmem_is_ashmem(fd, false) >= 0;
}

int __memfd_create_region(const char* name, size_t size) {
    // This code needs to build on API levels before 30,
    // so we can't use the libc wrapper.
    android::base::unique_fd fd(syscall(__NR_memfd_create, name, MFD_CLOEXEC | MFD_ALLOW_SEALING));

    if (fd == -1) {
        ALOGE("memfd_create(%s, %zd) failed: %m", name, size);
        return -1;
    }

    if (ftruncate(fd, size) == -1) {
        ALOGE("ftruncate(%s, %zd) failed for memfd creation: %m", name, size);
        return -1;
    }

    // forbid size changes to match ashmem behaviour
    if (fcntl(fd, F_ADD_SEALS, F_SEAL_GROW | F_SEAL_SHRINK) == -1) {
        ALOGE("memfd_create(%s, %zd) F_ADD_SEALS failed: %m", name, size);
        return -1;
    }

    if (debug_log) {
        ALOGE("memfd_create(%s, %zd) success. fd=%d", name, size, fd.get());
    }
    return fd.release();
}

int __ashmem_create_region(const char* name, size_t size) {
    android::base::unique_fd fd(__ashmem_open());
    if (!fd.ok() || TEMP_FAILURE_RETRY(ioctl(fd, ASHMEM_SET_NAME, name)) < 0 ||
        TEMP_FAILURE_RETRY(ioctl(fd, ASHMEM_SET_SIZE, size)) < 0) {
        return -1;
    }
    return fd.release();
}

/*
 * ashmem_create_region - creates a new ashmem region and returns the file
 * descriptor, or <0 on error
 *
 * `name' is an optional label to give the region (visible in /proc/pid/maps)
 * `size' is the size of the region, in page-aligned bytes
 */
int ashmem_create_region(const char* name, size_t size) {
    if (name == NULL) name = "none";

    auto create_region = use_memfd() ? __memfd_create_region : __ashmem_create_region;
    return create_region(name, size);
}

static int memfd_set_prot_region(int fd, int prot) {
    int seals = fcntl(fd, F_GET_SEALS);
    if (seals == -1) {
        ALOGE("memfd_set_prot_region(%d, %d): F_GET_SEALS failed: %m", fd, prot);
        return -1;
    }

    if (prot & PROT_WRITE) {
        /* Now we want the buffer to be read-write, let's check if the buffer
         * has been previously marked as read-only before, if so return error
         */
        if (seals & F_SEAL_FUTURE_WRITE) {
            ALOGE("memfd_set_prot_region(%d, %d): region is write protected", fd, prot);
            errno = EINVAL;  // inline with ashmem error code, if already in
                             // read-only mode
            return -1;
        }
        return 0;
    }

    /* We would only allow read-only for any future file operations */
    if (fcntl(fd, F_ADD_SEALS, F_SEAL_FUTURE_WRITE) == -1) {
        ALOGE("memfd_set_prot_region(%d, %d): F_SEAL_FUTURE_WRITE seal failed: %m", fd, prot);
        return -1;
    }

    return 0;
}

int ashmem_set_prot_region(int fd, int prot) {
    if (is_memfd_fd(fd)) {
        return memfd_set_prot_region(fd, prot);
    }

    return __ashmem_check_failure(fd, TEMP_FAILURE_RETRY(ioctl(fd, ASHMEM_SET_PROT_MASK, prot)));
}

static int do_pin(int op, int fd, size_t offset, size_t length) {
    static bool already_warned_about_pin_deprecation = false;
    if (!already_warned_about_pin_deprecation || debug_log) {
        ALOGE("Pinning is deprecated since Android Q. Please use trim or other methods.");
        already_warned_about_pin_deprecation = true;
    }

    if (is_memfd_fd(fd)) {
        return 0;
    }

    // TODO: should LP64 reject too-large offset/len?
    ashmem_pin pin = { static_cast<uint32_t>(offset), static_cast<uint32_t>(length) };
    return __ashmem_check_failure(fd, TEMP_FAILURE_RETRY(ioctl(fd, op, &pin)));
}

int ashmem_pin_region(int fd, size_t offset, size_t length) {
    return do_pin(ASHMEM_PIN, fd, offset, length);
}

int ashmem_unpin_region(int fd, size_t offset, size_t length) {
    return do_pin(ASHMEM_UNPIN, fd, offset, length);
}

int ashmem_get_size_region(int fd) {
    if (is_memfd_fd(fd)) {
        struct stat sb;
        if (fstat(fd, &sb) == -1) {
            ALOGE("ashmem_get_size_region(%d): fstat failed: %m", fd);
            return -1;
        }
        return sb.st_size;
    }

    return __ashmem_check_failure(fd, TEMP_FAILURE_RETRY(ioctl(fd, ASHMEM_GET_SIZE, NULL)));
}
