/*
* Copyright (c) 2026 Huawei Device Co., Ltd.
* 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.
*/
#ifndef JSVM_SCOPE_H
#define JSVM_SCOPE_H
#include <cstdint>
#include <mutex>
#include "jsvm.h"
#include "dfx/jsvm_misuse_reporter.h"
#include "jsvm_util.h"
namespace v8impl {
// Controls whether an IsolateScope must enter the isolate or can reuse the
// already-current TLS isolate.
enum class IsolateEnterMode {
ALWAYS_ENTER, // Always enter the isolate
ENTER_IF_MISMATCH, // Enter the isolate only if the given isolate is mismatched with TLS isolate
};
// Returns the OS-visible thread id used in diagnostics. The implementation
// caches the value per thread to avoid a syscall on hot scope paths.
uint32_t CurrentThreadId();
// Tracks the thread that owns JSVM-created v8::Isolate::Scope entries for a
// single isolate. It is intentionally small: one owner tid plus a re-entrant
// depth counter protected by a mutex.
//
// This is a JSVM-side misuse detector, not a V8 isolate lock. A successful
// acquire only means JSVM will allow this thread to construct an
// v8::Isolate::Scope; it does not make arbitrary concurrent V8 access safe.
class IsolateOwner final {
public:
// Acquires ownership for the current thread. Re-entrant acquisition from the
// same thread is allowed and increments enterDepth_.
bool TryAcquire(uint32_t currentTid, const char* apiName);
// Releases one ownership level. Only the owning thread may release it.
bool TryRelease(uint32_t currentTid, const char* apiName);
uint32_t GetTid() const;
IsolateOwner(const IsolateOwner&) = delete;
IsolateOwner& operator=(const IsolateOwner&) = delete;
IsolateOwner() = default;
~IsolateOwner() = default;
private:
// Protects ownerTid_ and enterDepth_. Isolate ownership can be observed from
// different JSVM API entry threads.
mutable std::mutex mutex_;
// OS-visible tid of the thread that opened the outermost JSVM-created
// Isolate::Scope. Zero means there is currently no owner.
uint32_t ownerTid_ = 0;
// Re-entrant ownership depth for ownerTid_. A zero depth means no owner.
uint32_t enterDepth_ = 0;
};
IsolateOwner* GetIsolateOwner(v8::Isolate* isolate);
struct VmScopePolicy final {
// Explicit VM scopes must preserve V8 Enter/Exit nesting even when the TLS
// isolate already matches the target isolate.
static constexpr IsolateEnterMode enterMode = IsolateEnterMode::ALWAYS_ENTER;
};
struct ApiCompatPolicy final {
// API compatibility scopes only repair a missing or mismatched TLS isolate.
// If the target isolate is already current, the fast path does not enter.
static constexpr IsolateEnterMode enterMode = IsolateEnterMode::ENTER_IF_MISMATCH;
};
// Thin RAII wrapper around v8::Isolate::Scope with JSVM-side owner tracking.
//
// Policy controls whether the wrapper must always enter the isolate or may use
// the current TLS isolate as a fast path. Construction failure is recorded
// internally through entered_/canAccessV8_; regular JSVM API access scopes do
// not propagate a new public error code from this wrapper.
template<typename Policy>
class IsolateScope final {
public:
explicit IsolateScope(v8::Isolate* isolate, const char* apiName = nullptr)
: isolate_(isolate),
owner_(GetIsolateOwner(isolate)),
apiName_(apiName),
scopeTid_(CurrentThreadId())
{
if (UNLIKELY(isolate_ == nullptr || owner_ == nullptr)) {
return;
}
if (!ShouldEnter()) {
// Fast path for API compatibility scopes. No new v8::Isolate::Scope
// is created, but V8 access is valid because TLS already matches.
tlsReady_ = true;
return;
}
uint32_t currentTid = scopeTid_;
if (UNLIKELY(!owner_->TryAcquire(currentTid, apiName_))) {
// Another thread owns this isolate in the JSVM tracker. Do not force
// v8::Isolate::Enter here; doing so can turn a latent misuse into a
// deterministic V8 CHECK failure.
ApiMisuseReporter::Report(ApiMisuseKind::E03_VM_SCOPE_CROSS_THREAD, apiName, owner_->GetTid(), currentTid);
return;
}
// The owner tracker must be acquired before entering V8 so a racing
// thread can observe and reject cross-thread isolate enter attempts.
new (ScopeStorage()) v8::Isolate::Scope(isolate_);
entered_ = true;
tlsReady_ = true;
}
~IsolateScope()
{
if (!entered_) {
return;
}
ScopeStorage()->~Scope();
uint32_t currentTid = CurrentThreadId();
owner_->TryRelease(currentTid, apiName_);
entered_ = false;
}
IsolateScope(const IsolateScope&) = delete;
IsolateScope& operator=(const IsolateScope&) = delete;
bool Entered() const
{
return entered_;
}
// Get the tid at the moment the isolate scope is opened
uint32_t GetOpenTid() const
{
return scopeTid_;
}
// Get the isolate at the moment the isolate scope is opened
v8::Isolate* GetOpenIsolate() const
{
return isolate_;
}
bool IsTlsReady() const
{
return tlsReady_;
}
private:
using Scope = v8::Isolate::Scope;
bool ShouldEnter() const
{
if constexpr (Policy::enterMode == IsolateEnterMode::ALWAYS_ENTER) {
return true;
}
if constexpr (Policy::enterMode == IsolateEnterMode::ENTER_IF_MISMATCH) {
return v8::Isolate::TryGetCurrent() != isolate_;
}
return false;
}
Scope* ScopeStorage()
{
return reinterpret_cast<Scope*>(isolateScopeStorage_);
}
v8::Isolate* isolate_ = nullptr;
// Cached owner tracker. The destructor uses this pointer instead of
// resolving isolate data again, because isolate embedder data may already be
// changing during shutdown paths.
IsolateOwner* owner_ = nullptr;
// API name used only for diagnostics. It is normally a static __func__
// pointer, so storing it does not allocate.
const char* apiName_ = nullptr;
// OS-visible tid that constructed this scope wrapper. Explicit close paths
// use it to detect cross-thread scope lifecycle misuse.
uint32_t scopeTid_ = 0;
// True only when this object actually constructed v8::Isolate::Scope.
bool entered_ = false;
// True when the current thread can safely access v8 TLS isolate: either
// TLS already matched the isolate or this wrapper entered the isolate.
bool tlsReady_ = false;
// Inline storage for v8::Isolate::Scope. It is constructed only when
// entered_ becomes true.
alignas(Scope) unsigned char isolateScopeStorage_[sizeof(Scope)];
};
} // namespace v8impl
#endif // JSVM_SCOPE_H