PR-URL: https://github.com/nodejs/node/pull/61898 Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com> Reviewed-By: Filip Skokan <panva.ip@gmail.com> Reviewed-By: Rafael Gonzaga <rafael.nunu@hotmail.com> Reviewed-By: Chengzhong Wu <legendecas@gmail.com>
170 lines
7.2 KiB
C++
170 lines
7.2 KiB
C++
// Copyright 2024 the V8 project authors. All rights reserved.
|
|
// Use of this source code is governed by a BSD-style license that can be
|
|
// found in the LICENSE file.
|
|
|
|
#ifndef INCLUDE_V8_SANDBOX_H_
|
|
#define INCLUDE_V8_SANDBOX_H_
|
|
|
|
#include <cstdint>
|
|
|
|
#include "v8-internal.h" // NOLINT(build/include_directory)
|
|
#include "v8config.h" // NOLINT(build/include_directory)
|
|
|
|
namespace v8 {
|
|
|
|
/**
|
|
* A pointer tag used for wrapping and unwrapping `CppHeap` pointers as used
|
|
* with JS API wrapper objects that rely on `v8::Object::Wrap()` and
|
|
* `v8::Object::Unwrap()`.
|
|
*
|
|
* The CppHeapPointers use a range-based type checking scheme, where on access
|
|
* to a pointer, the actual type of the pointer is checked to be within a
|
|
* specified range of types. This allows supporting type hierarchies, where a
|
|
* type check for a supertype must succeed for any subtype.
|
|
*
|
|
* The tag is currently in practice limited to 15 bits since it needs to fit
|
|
* together with a marking bit into the unused parts of a pointer.
|
|
*/
|
|
enum class CppHeapPointerTag : uint16_t {
|
|
kFirstTag = 0,
|
|
kNullTag = 0,
|
|
|
|
/**
|
|
* The lower type ids are reserved for the embedder to assign. For that, the
|
|
* main requirement is that all (transitive) child classes of a given parent
|
|
* class have type ids in the same range, and that there are no unrelated
|
|
* types in that range. For example, given the following type hierarchy:
|
|
*
|
|
* A F
|
|
* / \
|
|
* B E
|
|
* / \
|
|
* C D
|
|
*
|
|
* a potential type id assignment that satistifes these requirements is
|
|
* {C: 0, D: 1, B: 2, A: 3, E: 4, F: 5}. With that, the type check for type A
|
|
* would check for the range [0, 4], while the check for B would check range
|
|
* [0, 2], and for F it would simply check [5, 5].
|
|
*
|
|
* In addition, there is an option for performance tweaks: if the size of the
|
|
* type range corresponding to a supertype is a power of two and starts at a
|
|
* power of two (e.g. [0x100, 0x13f]), then the compiler can often optimize
|
|
* the type check to use even fewer instructions (essentially replace a AND +
|
|
* SUB with a single AND).
|
|
*/
|
|
|
|
kDefaultTag = 0x7000,
|
|
|
|
kZappedEntryTag = 0x7ffd,
|
|
kEvacuationEntryTag = 0x7ffe,
|
|
kFreeEntryTag = 0x7fff,
|
|
// The tags are limited to 15 bits, so the last tag is 0x7fff.
|
|
kLastTag = 0x7fff,
|
|
};
|
|
|
|
using CppHeapPointerTagRange = internal::TagRange<CppHeapPointerTag>;
|
|
|
|
constexpr CppHeapPointerTagRange kAnyCppHeapPointer(
|
|
CppHeapPointerTag::kFirstTag, CppHeapPointerTag::kLastTag);
|
|
|
|
/**
|
|
* Hardware support for the V8 Sandbox.
|
|
*
|
|
* This is an experimental feature that may change or be removed without
|
|
* further notice. Use at your own risk.
|
|
*/
|
|
class SandboxHardwareSupport {
|
|
public:
|
|
/**
|
|
* Initialize sandbox hardware support. This needs to be called before
|
|
* creating any thread that might access sandbox memory since it sets up
|
|
* hardware permissions to the memory that will be inherited on clone.
|
|
*/
|
|
V8_EXPORT static void InitializeBeforeThreadCreation();
|
|
};
|
|
|
|
namespace internal {
|
|
|
|
#ifdef V8_COMPRESS_POINTERS
|
|
V8_INLINE static Address* GetCppHeapPointerTableBase(v8::Isolate* isolate) {
|
|
Address addr = reinterpret_cast<Address>(isolate) +
|
|
Internals::kIsolateCppHeapPointerTableOffset +
|
|
Internals::kExternalEntityTableBasePointerOffset;
|
|
return *reinterpret_cast<Address**>(addr);
|
|
}
|
|
#endif // V8_COMPRESS_POINTERS
|
|
|
|
template <typename T>
|
|
V8_INLINE static T* ReadCppHeapPointerField(v8::Isolate* isolate,
|
|
Address heap_object_ptr, int offset,
|
|
CppHeapPointerTagRange tag_range) {
|
|
// This is a specialized version of the the CppHeapPointerTable accessors
|
|
// which (1) allows the code to be inlined into the callers for performance
|
|
// and (2) is optimized for code size as there are a huge number of callers
|
|
// from auto-generated bindings code.
|
|
|
|
#ifdef V8_COMPRESS_POINTERS
|
|
const CppHeapPointerHandle handle =
|
|
Internals::ReadRawField<CppHeapPointerHandle>(heap_object_ptr, offset);
|
|
const uint32_t index = handle >> kExternalPointerIndexShift;
|
|
const Address* table = GetCppHeapPointerTableBase(isolate);
|
|
const std::atomic<Address>* ptr =
|
|
reinterpret_cast<const std::atomic<Address>*>(&table[index]);
|
|
Address entry = std::atomic_load_explicit(ptr, std::memory_order_relaxed);
|
|
|
|
// Note: the cast to uint32_t is important here. Otherwise, the uint16_t's
|
|
// would be promoted to int in the range check below, which would result in
|
|
// undefined behavior (signed integer underflow) if the actual value is less
|
|
// than the lower bound. Then, the compiler would take advantage of the
|
|
// undefined behavior and turn the range check into a simple
|
|
// `actual_tag <= last_tag` comparison, which is incorrect.
|
|
uint32_t actual_tag = static_cast<uint16_t>(entry);
|
|
// The actual_tag is shifted to the left by one and contains the marking
|
|
// bit in the LSB. To ignore that during the type check, simply add one to
|
|
// the (shifted) range.
|
|
constexpr int kTagShift = internal::kCppHeapPointerTagShift;
|
|
uint32_t first_tag = static_cast<uint32_t>(tag_range.first) << kTagShift;
|
|
uint32_t last_tag = (static_cast<uint32_t>(tag_range.last) << kTagShift) + 1;
|
|
if (V8_LIKELY(actual_tag >= first_tag && actual_tag <= last_tag)) {
|
|
entry = entry >> kCppHeapPointerPayloadShift;
|
|
} else {
|
|
// If the type check failed, we simply return nullptr here. That way:
|
|
// 1. The null handle always results in nullptr being returned here, which
|
|
// is a desired property. Otherwise, we would need an explicit check for
|
|
// the null handle above, and therefore an additional branch. This
|
|
// works because the 0th entry of the table always contains nullptr
|
|
// tagged with the null tag (i.e. an all-zeros entry). As such,
|
|
// regardless of whether the type check succeeds, the result will
|
|
// always be nullptr.
|
|
// 2. The returned pointer is guaranteed to crash even on platforms with
|
|
// top byte ignore (TBI), such as Arm64. The alternative would be to
|
|
// simply return the original entry with the left-shifted payload.
|
|
// However, due to TBI, an access to that may not always result in a
|
|
// crash (specifically, if the second most significant byte happens to
|
|
// be zero). In addition, there shouldn't be a difference on Arm64
|
|
// between returning nullptr or the original entry, since it will
|
|
// simply compile to a `csel x0, x8, xzr, lo` instead of a
|
|
// `csel x0, x10, x8, lo` instruction.
|
|
// 3. The machine code sequence ends up being pretty short, which is
|
|
// important here as this code will be inlined into a lot of functions.
|
|
entry = 0;
|
|
}
|
|
return reinterpret_cast<T*>(entry);
|
|
#else // !V8_COMPRESS_POINTERS
|
|
return reinterpret_cast<T*>(
|
|
Internals::ReadRawField<Address>(heap_object_ptr, offset));
|
|
#endif // !V8_COMPRESS_POINTERS
|
|
}
|
|
|
|
// TODO(saelo): temporary workaround needed to introduce range-based type
|
|
// checks for the external pointer table. See comment above
|
|
// ExternalPointerCanBeEmpty(ExternalPointerTagRange) function for details.
|
|
V8_INLINE static constexpr bool ExternalPointerCanBeEmpty(
|
|
CppHeapPointerTagRange tag_range) {
|
|
return true;
|
|
}
|
|
|
|
} // namespace internal
|
|
} // namespace v8
|
|
|
|
#endif // INCLUDE_V8_SANDBOX_H_
|