/*
* Copyright (C) 2012-2023 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 "JITThunks.h"
#if ENABLE(JIT)
#include "CommonSlowPaths.h"
#include "JIT.h"
#include "JITCode.h"
#include "JSCJSValueInlines.h"
#include "LLIntThunks.h"
#include "SlowPathCall.h"
#include "ThunkGenerators.h"
#include "VM.h"
#include "YarrJIT.h"
#include
#include
#include
#include
WTF_ALLOW_UNSAFE_BUFFER_USAGE_BEGIN
namespace JSC {
WTF_MAKE_TZONE_ALLOCATED_IMPL(JITThunks);
JITThunks::JITThunks() = default;
JITThunks::~JITThunks() = default;
using SharedCommonThunks = std::array;
static const SharedCommonThunks& sharedCommonThunks()
{
static LazyNeverDestroyed thunks;
static std::once_flag onceKey;
std::call_once(onceKey, [] {
thunks.construct();
unsigned index = 0;
#define JSC_DEFINE_SHARED_JIT_THUNK(name, func) thunks.get()[index++] = func();
JSC_FOR_EACH_VM_INDEPENDENT_COMMON_THUNK(JSC_DEFINE_SHARED_JIT_THUNK)
#undef JSC_DEFINE_SHARED_JIT_THUNK
});
return thunks.get();
}
static ThunkGenerator generatorForLazyCommonThunk(CommonJITThunkID thunkID)
{
switch (thunkID) {
#define JSC_CASE_COMMON_JIT_THUNK(name, func) \
case CommonJITThunkID::name: return func;
JSC_FOR_EACH_VM_DEPENDENT_LAZY_COMMON_THUNK(JSC_CASE_COMMON_JIT_THUNK)
#undef JSC_CASE_COMMON_JIT_THUNK
default:
break;
}
RELEASE_ASSERT_NOT_REACHED();
}
void JITThunks::initialize(VM& vm)
{
ASSERT(!isCompilationThread());
sharedCommonThunks();
#define JSC_DEFINE_COMMON_JIT_THUNK(name, func) \
m_eagerCommonThunks[static_cast(CommonJITThunkID::name) - numberOfVMIndependentCommonThunkIDs] = func(vm);
JSC_FOR_EACH_VM_DEPENDENT_EAGER_COMMON_THUNK(JSC_DEFINE_COMMON_JIT_THUNK)
#undef JSC_DEFINE_COMMON_JIT_THUNK
}
static inline NativeExecutable& NODELETE getMayBeDyingNativeExecutable(const Weak& weak)
{
// This never gets Deleted / Empty slots.
WeakImpl* impl = weak.unsafeImpl();
ASSERT(impl);
// We have a callback removing entry when finalizing. This means that we never hold Deallocated entry in HashSet.
ASSERT(impl->state() != WeakImpl::State::Deallocated);
// Never use jsCast here. This is possible that this value is "Dead" but not "Finalized" yet. In this case,
// we can still access to non-JS data, as we are doing in a finalize callback.
auto* executable = static_cast(impl->jsValue().asCell());
ASSERT(executable);
return *executable;
}
inline unsigned JITThunks::WeakNativeExecutableHash::hash(const NativeExecutable* executable)
{
return hash(executable->function(), executable->constructor(), executable->implementationVisibility(), executable->length(), executable->name());
}
inline unsigned JITThunks::WeakNativeExecutableHash::hash(const Weak& key)
{
return hash(&getMayBeDyingNativeExecutable(key));
}
inline bool JITThunks::WeakNativeExecutableHash::equal(const NativeExecutable& a, const NativeExecutable& b)
{
if (&a == &b)
return true;
return a.function() == b.function() && a.constructor() == b.constructor() && a.implementationVisibility() == b.implementationVisibility() && a.length() == b.length() && a.name() == b.name();
}
inline bool JITThunks::WeakNativeExecutableHash::equal(const Weak& a, const Weak& b)
{
return equal(getMayBeDyingNativeExecutable(a), getMayBeDyingNativeExecutable(b));
}
inline bool JITThunks::WeakNativeExecutableHash::equal(const Weak& a, const NativeExecutable* bExecutable)
{
return equal(getMayBeDyingNativeExecutable(a), *bExecutable);
}
inline bool JITThunks::WeakNativeExecutableHash::equal(const Weak& a, const HostFunctionKey& b)
{
auto& aExecutable = getMayBeDyingNativeExecutable(a);
return aExecutable.function() == std::get(b) && aExecutable.constructor() == std::get(b) && aExecutable.implementationVisibility() == std::get(b) && aExecutable.length() == std::get(b) && aExecutable.name() == std::get(b);
}
CodePtr JITThunks::ctiNativeCall(VM& vm)
{
ASSERT(Options::useJIT());
return ctiStub(vm, CommonJITThunkID::NativeCall).code();
}
CodePtr JITThunks::ctiNativeCallWithDebuggerHook(VM& vm)
{
ASSERT(Options::useJIT());
return ctiStub(vm, nativeCallWithDebuggerHookGenerator).code();
}
CodePtr JITThunks::ctiNativeConstruct(VM& vm)
{
ASSERT(Options::useJIT());
return ctiStub(vm, CommonJITThunkID::NativeConstruct).code();
}
CodePtr JITThunks::ctiNativeConstructWithDebuggerHook(VM& vm)
{
ASSERT(Options::useJIT());
return ctiStub(vm, nativeConstructWithDebuggerHookGenerator).code();
}
CodePtr JITThunks::ctiNativeTailCall(VM& vm)
{
ASSERT(Options::useJIT());
return ctiStub(vm, CommonJITThunkID::NativeTailCall).code();
}
CodePtr JITThunks::ctiNativeTailCallWithoutSavedTags(VM& vm)
{
ASSERT(Options::useJIT());
return ctiStub(vm, CommonJITThunkID::NativeTailCallWithoutSavedTags).code();
}
CodePtr JITThunks::ctiInternalFunctionCall(VM& vm)
{
ASSERT(Options::useJIT());
return ctiStub(vm, CommonJITThunkID::InternalFunctionCall).code();
}
CodePtr JITThunks::ctiInternalFunctionConstruct(VM& vm)
{
ASSERT(Options::useJIT());
return ctiStub(vm, CommonJITThunkID::InternalFunctionConstruct).code();
}
template
MacroAssemblerCodeRef JITThunks::ctiStubImpl(ThunkGenerator key, GenerateThunk generateThunk)
{
Locker locker { m_lock };
auto handleEntry = [&] (Entry& entry) {
if (entry.needsCrossModifyingCodeFence && !isCompilationThread()) {
// The main thread will issue a crossModifyingCodeFence before running
// any code the compiler thread generates, including any thunks that they
// generate. However, the main thread may grab the thunk a compiler thread
// generated before we've issued that crossModifyingCodeFence. Hence, we
// conservatively say that when the main thread grabs a thunk generated
// from a compiler thread for the first time, it issues a crossModifyingCodeFence.
WTF::crossModifyingCodeFence();
entry.needsCrossModifyingCodeFence = false;
}
return MacroAssemblerCodeRef(*entry.handle);
};
{
auto iter = m_ctiStubMap.find(key);
if (iter != m_ctiStubMap.end())
return handleEntry(iter->value);
}
// We do two lookups on first addition to the hash table because generateThunk may add to it.
MacroAssemblerCodeRef codeRef = generateThunk();
bool needsCrossModifyingCodeFence = isCompilationThread();
auto addResult = m_ctiStubMap.add(key, Entry { PackedRefPtr(codeRef.executableMemory()), needsCrossModifyingCodeFence });
RELEASE_ASSERT(addResult.isNewEntry); // Thunks aren't recursive, so anything we generated transitively shouldn't have generated 'key'.
return handleEntry(addResult.iterator->value);
}
MacroAssemblerCodeRef JITThunks::ctiStub(VM& vm, ThunkGenerator generator)
{
return ctiStubImpl(generator, [&] {
return generator(vm);
});
}
MacroAssemblerCodeRef JITThunks::ctiStub(VM& vm, CommonJITThunkID thunkID)
{
unsigned index = static_cast(thunkID);
if (index < numberOfVMIndependentCommonThunkIDs)
return sharedCommonThunks()[index];
unsigned vmDependentIndex = index - numberOfVMIndependentCommonThunkIDs;
if (vmDependentIndex < numberOfVMDependentEagerCommonThunkIDs) {
auto result = m_eagerCommonThunks[vmDependentIndex];
ASSERT(result);
return result;
}
return lazyCommonThunk(vm, thunkID);
}
MacroAssemblerCodeRef JITThunks::lazyCommonThunk(VM& vm, CommonJITThunkID thunkID)
{
auto& thunk = m_lazyCommonThunks[static_cast(thunkID) - numberOfVMIndependentCommonThunkIDs - numberOfVMDependentEagerCommonThunkIDs];
auto state = thunk.state.load(std::memory_order_acquire);
if (state == LazyThunkState::NotGenerated) {
Locker locker { thunk.lock };
state = thunk.state.loadRelaxed();
if (state == LazyThunkState::NotGenerated) {
thunk.codeRef = generatorForLazyCommonThunk(thunkID)(vm);
ASSERT(thunk.codeRef);
state = isCompilationThread() ? LazyThunkState::GeneratedOnCompilationThread : LazyThunkState::Generated;
thunk.state.store(state, std::memory_order_release);
}
}
if (state == LazyThunkState::GeneratedOnCompilationThread && !isCompilationThread()) {
// The main thread will issue a crossModifyingCodeFence before running
// any code the compiler thread generates, including any thunks that they
// generate. However, the main thread may grab the thunk a compiler thread
// generated before we've issued that crossModifyingCodeFence. Hence, we
// conservatively say that when the main thread grabs a thunk generated
// from a compiler thread for the first time, it issues a crossModifyingCodeFence.
WTF::crossModifyingCodeFence();
thunk.state.store(LazyThunkState::Generated, std::memory_order_release);
}
return thunk.codeRef;
}
MacroAssemblerCodeRef JITThunks::ctiSlowPathFunctionStub(VM& vm, SlowPathFunction slowPathFunction)
{
auto key = std::bit_cast(slowPathFunction);
return ctiStubImpl(key, [&] {
return JITSlowPathCall::generateThunk(vm, slowPathFunction);
});
}
struct JITThunks::HostKeySearcher {
static unsigned NODELETE hash(const HostFunctionKey& key) { return WeakNativeExecutableHash::hash(key); }
static bool NODELETE equal(const Weak& a, const HostFunctionKey& b) { return WeakNativeExecutableHash::equal(a, b); }
};
struct JITThunks::NativeExecutableTranslator {
static unsigned NODELETE hash(const NativeExecutable* key) { return WeakNativeExecutableHash::hash(key); }
static bool NODELETE equal(const Weak& a, const NativeExecutable* b) { return WeakNativeExecutableHash::equal(a, b); }
static void translate(Weak& location, NativeExecutable* executable, unsigned)
{
location = Weak(executable, executable->vm().jitStubs.get());
}
};
void JITThunks::finalize(Handle handle, void*)
{
auto* nativeExecutable = static_cast(handle.get().asCell());
auto hostFunctionKey = std::make_tuple(nativeExecutable->function(), nativeExecutable->constructor(), nativeExecutable->implementationVisibility(), nativeExecutable->length(), nativeExecutable->name());
{
AssertNoGC assertNoGC;
auto iterator = m_nativeExecutableSet.find(hostFunctionKey);
// Because this finalizer is called, this means that we still have dead Weak in m_nativeExecutableSet.
ASSERT(iterator != m_nativeExecutableSet.end());
ASSERT(iterator->unsafeImpl()->state() == WeakImpl::State::Finalized);
m_nativeExecutableSet.remove(iterator);
}
}
NativeExecutable* JITThunks::hostFunctionStub(VM& vm, TaggedNativeFunction function, TaggedNativeFunction constructor, ImplementationVisibility implementationVisibility, unsigned length, const String& name)
{
return hostFunctionStub(vm, function, constructor, nullptr, implementationVisibility, NoIntrinsic, nullptr, length, name);
}
NativeExecutable* JITThunks::hostFunctionStub(VM& vm, TaggedNativeFunction function, TaggedNativeFunction constructor, ThunkGenerator generator, ImplementationVisibility implementationVisibility, Intrinsic intrinsic, const DOMJIT::Signature* signature, unsigned length, const String& name)
{
ASSERT(!isCompilationThread());
ASSERT(Options::useJIT());
auto hostFunctionKey = std::make_tuple(function, constructor, implementationVisibility, length, name);
{
AssertNoGC assertNoGC;
auto iterator = m_nativeExecutableSet.find(hostFunctionKey);
if (iterator != m_nativeExecutableSet.end()) {
// It is possible that this returns Weak which is Dead, but not finalized.
// We should not use this reference to store value created in the subsequent sequence, since allocating NativeExecutable can cause GC, which changes this Set.
if (auto* executable = iterator->get())
return executable;
}
}
RefPtr forCall;
if (generator) {
MacroAssemblerCodeRef entry = generator(vm).retagged();
forCall = adoptRef(new DirectJITCode(entry, entry.code(), JITType::HostCallThunk, intrinsic));
} else if (signature)
forCall = adoptRef(new NativeDOMJITCode(MacroAssemblerCodeRef::createSelfManagedCodeRef(ctiNativeCall(vm).retagged()), JITType::HostCallThunk, intrinsic, signature));
else
forCall = adoptRef(new NativeJITCode(MacroAssemblerCodeRef::createSelfManagedCodeRef(ctiNativeCall(vm).retagged()), JITType::HostCallThunk, intrinsic));
Ref forConstruct = adoptRef(*new NativeJITCode(MacroAssemblerCodeRef::createSelfManagedCodeRef(ctiNativeConstruct(vm).retagged()), JITType::HostCallThunk, NoIntrinsic));
NativeExecutable* nativeExecutable = NativeExecutable::create(vm, forCall.releaseNonNull(), function, WTF::move(forConstruct), constructor, implementationVisibility, length, name);
{
AssertNoGC assertNoGC;
auto addResult = m_nativeExecutableSet.add(nativeExecutable);
if (!addResult.isNewEntry) {
// Override the existing Weak with the new one since it is dead.
ASSERT(!*addResult.iterator);
*addResult.iterator = Weak(nativeExecutable, this);
ASSERT(*addResult.iterator);
#if ASSERT_ENABLED
auto iterator = m_nativeExecutableSet.find(hostFunctionKey);
ASSERT(iterator != m_nativeExecutableSet.end());
ASSERT(iterator->get() == nativeExecutable);
ASSERT(iterator->unsafeImpl()->state() == WeakImpl::State::Live);
#endif
}
}
return nativeExecutable;
}
NativeExecutable* JITThunks::hostFunctionStub(VM& vm, TaggedNativeFunction function, ThunkGenerator generator, ImplementationVisibility implementationVisibility, Intrinsic intrinsic, unsigned length, const String& name)
{
return hostFunctionStub(vm, function, callHostFunctionAsConstructor, generator, implementationVisibility, intrinsic, nullptr, length, name);
}
} // namespace JSC
WTF_ALLOW_UNSAFE_BUFFER_USAGE_END
#endif // ENABLE(JIT)