/*
 * Copyright (C) 2019-2025 Apple Inc. All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY
 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 * PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL APPLE INC. OR
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
 * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

#include "config.h"
#include "Integrity.h"

#include "APICast.h"
#include "CellSize.h"
#include "IntegrityInlines.h"
#include "JSCast.h"
#include "JSCellInlines.h"
#include "JSGlobalObject.h"
#include "Options.h"
#include "VMManager.h"
#include <wtf/DataLog.h>

#if OS(DARWIN)
#include <wtf/darwin/OSLogPrintStream.h>
#endif

namespace JSC {
namespace Integrity {

namespace IntegrityInternal {
static constexpr bool verbose = false;
}

PrintStream& logFile()
{
#if OS(DARWIN)
    static PrintStream* s_file;
    static std::once_flag once;
    std::call_once(once, [] {
        // We want to use OS_LOG_TYPE_ERROR because we want to guarantee that the log makes it into
        // the file system, and is not potentially stuck in some memory buffer. Integrity audit logs
        // are used for debugging error states. So, this is an appropriate use of OS_LOG_TYPE_ERROR.
        s_file = OSLogPrintStream::open("com.apple.JavaScriptCore", "Integrity", OS_LOG_TYPE_ERROR).release();
    });
    return *s_file;
#else
    return WTF::dataFile();
#endif
}

WTF_ALLOW_UNSAFE_BUFFER_USAGE_BEGIN

void logF(const char* format, ...)
{
    va_list argList;
    va_start(argList, format);
    logFile().vprintf(format, argList);
    va_end(argList);
}

void logLnF(const char* format, ...)
{
    va_list argList;
    va_start(argList, format);
    logFile().vprintf(format, argList);
    va_end(argList);
    logFile().println();
}

WTF_ALLOW_UNSAFE_BUFFER_USAGE_END

Random::Random(VM& vm)
{
    reloadAndCheckShouldAuditSlow(vm);
}

bool Random::reloadAndCheckShouldAuditSlow(VM& vm)
{
    Locker locker { m_lock };

    if (!Options::randomIntegrityAuditRate()) {
        m_triggerBits = 0; // Never trigger, and don't bother reloading.
        if (IntegrityInternal::verbose)
            dataLogLn("disabled Integrity audits: trigger bits ", RawHex(m_triggerBits));
        return false;
    }

    // Reload the trigger bits.
    m_triggerBits = 1ull << 63;

    uint32_t threshold = UINT_MAX * Options::randomIntegrityAuditRate();
    for (int i = 0; i < numberOfTriggerBits; ++i) {
        bool trigger = vm.random().getUint32() <= threshold;
        m_triggerBits = m_triggerBits | (static_cast<uint64_t>(trigger) << i);
    }
    if (IntegrityInternal::verbose)
        dataLogLn("reloaded Integrity trigger bits ", RawHex(m_triggerBits));
    ASSERT(m_triggerBits >= (1ull << 63));
    return vm.random().getUint32() <= threshold;
}

void auditCellMinimallySlow(VM&, JSCell* cell)
{
    if (Gigacage::contains(cell)) {
        if (cell->type() != JSCellButterflyType) {
            if (IntegrityInternal::verbose)
                dataLogLn("Integrity ERROR: Bad cell ", RawPointer(cell), " ", JSValue(cell));
            CRASH();
        }
    }
}

#if USE(JSVALUE64)

#if ENABLE(EXTRA_INTEGRITY_CHECKS)
// toJS will trigger an audit if ENABLE(EXTRA_INTEGRITY_CHECKS).
#define DO_AUDIT(value) toJS(value)
#else
// Else, we'll need to explicit call doAudit.
#define DO_AUDIT(value) doAudit(toJS(value))
#endif

JSContextRef doAudit(JSContextRef ctx)
{
    IA_ASSERT(ctx, "NULL JSContextRef");
    DO_AUDIT(ctx);
    return ctx;
}

JSGlobalContextRef doAudit(JSGlobalContextRef ctx)
{
    IA_ASSERT(ctx, "NULL JSGlobalContextRef");
    DO_AUDIT(ctx);
    return ctx;
}

JSObjectRef doAudit(JSObjectRef objectRef)
{
    if (!objectRef)
        return objectRef;
    DO_AUDIT(objectRef);
    return objectRef;
}

JSValueRef doAudit(JSValueRef valueRef)
{
#if CPU(ADDRESS64)
    if (!valueRef)
        return valueRef;
    DO_AUDIT(valueRef);
#endif
    return valueRef;
}

#undef DO_AUDIT

JSValue doAudit(JSValue value)
{
    if (value.isCell())
        doAudit(value.asCell());
    return value;
}

bool Analyzer::analyzeVM(VM& vm, Analyzer::Action action)
{
    IA_ASSERT_WITH_ACTION(VMManager::isValidVM(&vm), {
        VMManager::dumpVMs();
        if (action == Action::LogAndCrash)
            RELEASE_ASSERT(VMManager::isValidVM(&vm));
        else
            return false;
    }, "Invalid VM %p", &vm);
    return true;
}

#if !VA_OPT_SUPPORTED

#define AUDIT_VERIFY(cond, format, ...) do { \
        IA_ASSERT_WITH_ACTION(cond, { \
            Integrity::logLnF("    cell %p", cell); \
            if (action == Action::LogAndCrash) \
                RELEASE_ASSERT((cond)); \
            else \
                return false; \
        }); \
    } while (false)

#else // not !VA_OPT_SUPPORTED

#define AUDIT_VERIFY(cond, format, ...) do { \
        IA_ASSERT_WITH_ACTION(cond, { \
            Integrity::logLnF("    cell %p", cell); \
            if (action == Action::LogAndCrash) \
                RELEASE_ASSERT((cond) __VA_OPT__(,) __VA_ARGS__); \
            else \
                return false; \
        }, format __VA_OPT__(,) __VA_ARGS__); \
    } while (false)

#endif // !VA_OPT_SUPPORTED

bool Analyzer::analyzeCell(VM& vm, JSCell* cell, Analyzer::Action action)
{
    AUDIT_VERIFY(isSanePointer(cell), "cell %p cell.type %d", cell, cell->type());

    size_t allocatorCellSize = 0;
    if (cell->isPreciseAllocation()) {
        PreciseAllocation& preciseAllocation = cell->preciseAllocation();
        AUDIT_VERIFY(&preciseAllocation.vm() == &vm,
            "cell %p cell.type %d preciseAllocation.vm %p vm %p", cell, cell->type(), &preciseAllocation.vm(), &vm);

        bool isValidPreciseAllocation = false;
        for (auto* i : vm.heap.objectSpace().preciseAllocations()) {
            if (i == &preciseAllocation) {
                isValidPreciseAllocation = true;
                break;
            }
        }
        AUDIT_VERIFY(isValidPreciseAllocation, "cell %p cell.type %d", cell, cell->type());

        allocatorCellSize = preciseAllocation.cellSize();
    } else {
        MarkedBlock& block = cell->markedBlock();
        MarkedBlock::Handle& blockHandle = block.handle();
        AUDIT_VERIFY(&block.vm() == &vm,
            "cell %p cell.type %d markedBlock.vm %p vm %p", cell, cell->type(), &block.vm(), &vm);

        uintptr_t blockStartAddress = reinterpret_cast<uintptr_t>(blockHandle.start());
        AUDIT_VERIFY(blockHandle.contains(cell),
            "cell %p cell.type %d markedBlock.start %p markedBlock.end %p", cell, cell->type(), blockHandle.start(), blockHandle.end());

        uintptr_t cellAddress = reinterpret_cast<uintptr_t>(cell);
        uintptr_t cellOffset = cellAddress - blockStartAddress;
        allocatorCellSize = block.cellSize();
        bool cellIsProperlyAligned = !(cellOffset % allocatorCellSize);
        AUDIT_VERIFY(cellIsProperlyAligned,
            "cell %p cell.type %d allocator.cellSize %zu", cell, cell->type(), allocatorCellSize);
    }

    JSType cellType = cell->type();
    if (cell->type() != JSCellButterflyType)
        AUDIT_VERIFY(!Gigacage::contains(cell), "cell %p cell.type %d", cell, cellType);

    WeakSet& weakSet = cell->cellContainer().weakSet();
    AUDIT_VERIFY(!weakSet.m_allocator || isSanePointer(weakSet.m_allocator),
        "cell %p cell.type %d weakSet.allocator %p", cell, cell->type(), weakSet.m_allocator);
    AUDIT_VERIFY(!weakSet.m_nextAllocator || isSanePointer(weakSet.m_nextAllocator),
        "cell %p cell.type %d weakSet.allocator %p", cell, cell->type(), weakSet.m_nextAllocator);

    // If we're currently destructing the cell, then we can't rely on its
    // structure being good. Skip the following tests which rely on structure.
    if (vm.currentlyDestructingCallbackObject == cell)
        return true;

    auto structureID = cell->structureID();

    Structure* structure = structureID.tryDecode();
    AUDIT_VERIFY(structure,
        "cell %p cell.type %d structureID.bits 0x%x", cell, cellType, structureID.bits());
    if (action == Analyzer::Action::LogAndCrash) {
        // structure should be pointing to readable memory. Force a read.
        WTF::opaque(*std::bit_cast<uintptr_t*>(structure));
    }

    const ClassInfo* classInfo = structure->classInfoForCells();
    AUDIT_VERIFY(cellType == structure->m_blob.type(),
        "cell %p cell.type %d structureBlob.type %d", cell, cellType, structure->m_blob.type());

    size_t size = cellSize(cell);
    AUDIT_VERIFY(size <= allocatorCellSize,
        "cell %p cell.type %d cell.size %zu allocator.cellSize %zu, classInfo.cellSize %u", cell, cellType, size, allocatorCellSize, classInfo->staticClassSize);
    if (isDynamicallySizedType(cellType)) {
        AUDIT_VERIFY(size >= classInfo->staticClassSize,
            "cell %p cell.type %d cell.size %zu classInfo.cellSize %u", cell, cellType, size, classInfo->staticClassSize);
    }

    if (cell->isObject()) {
        AUDIT_VERIFY(jsDynamicCast<JSObject*>(cell),
            "cell %p cell.type %d", cell, cell->type());
    }

    return true;
}

bool Analyzer::analyzeCell(JSCell* cell, Analyzer::Action action)
{
    if (!cell)
        return cell;

    JSValue value = JSValue::decode(static_cast<EncodedJSValue>(std::bit_cast<uintptr_t>(cell)));
    AUDIT_VERIFY(value.isCell(), "Invalid cell address: cell %p", cell);

    VM& vm = cell->vm();
    analyzeVM(vm, action);
    return analyzeCell(vm, cell, action);
}

#undef AUDIT_VERIFY

VM* doAuditSlow(VM* vm)
{
    Analyzer::analyzeVM(*vm, Analyzer::Action::LogAndCrash);
    return vm;
}

JSCell* doAudit(JSCell* cell)
{
    Analyzer::analyzeCell(cell, Analyzer::Action::LogAndCrash);
    return cell;
}

JSCell* doAudit(VM& vm, JSCell* cell)
{
    if (!cell)
        return cell;
    Analyzer::analyzeCell(vm, cell, Analyzer::Action::LogAndCrash);
    return cell;
}

bool verifyCell(JSCell* cell)
{
    bool valid = Analyzer::analyzeCell(cell, Analyzer::Action::LogOnly);
    Integrity::logLnF("Cell %p is %s", cell, valid ? "VALID" : "INVALID");
    return valid;
}

bool verifyCell(VM& vm, JSCell* cell)
{
    bool valid = Analyzer::analyzeCell(vm, cell, Analyzer::Action::LogOnly);
    Integrity::logLnF("Cell %p is %s", cell, valid ? "VALID" : "INVALID");
    return valid;
}

JSObject* doAudit(JSObject* object)
{
    if (!object)
        return object;
    JSCell* cell = doAudit(reinterpret_cast<JSCell*>(object));
    IA_ASSERT(cell->isObject(), "Invalid JSObject %p", object);
    return object;
}

JSGlobalObject* doAudit(JSGlobalObject* globalObject)
{
    doAudit(reinterpret_cast<JSCell*>(globalObject));
    IA_ASSERT(globalObject->isGlobalObject(), "Invalid JSGlobalObject %p", globalObject);
    return globalObject;
}

#endif // USE(JSVALUE64)

} // namespace Integrity
} // namespace JSC
