Coverage Report

Created: 2025-10-31 09:06

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/node/deps/v8/include/v8-sandbox.h
Line
Count
Source
1
// Copyright 2024 the V8 project authors. All rights reserved.
2
// Use of this source code is governed by a BSD-style license that can be
3
// found in the LICENSE file.
4
5
#ifndef INCLUDE_V8_SANDBOX_H_
6
#define INCLUDE_V8_SANDBOX_H_
7
8
#include <cstdint>
9
10
#include "v8-internal.h"  // NOLINT(build/include_directory)
11
#include "v8config.h"     // NOLINT(build/include_directory)
12
13
namespace v8 {
14
15
/**
16
 * A pointer tag used for wrapping and unwrapping `CppHeap` pointers as used
17
 * with JS API wrapper objects that rely on `v8::Object::Wrap()` and
18
 * `v8::Object::Unwrap()`.
19
 *
20
 * The CppHeapPointers use a range-based type checking scheme, where on access
21
 * to a pointer, the actual type of the pointer is checked to be within a
22
 * specified range of types. This allows supporting type hierarchies, where a
23
 * type check for a supertype must succeed for any subtype.
24
 *
25
 * The tag is currently in practice limited to 15 bits since it needs to fit
26
 * together with a marking bit into the unused parts of a pointer.
27
 */
28
enum class CppHeapPointerTag : uint16_t {
29
  kFirstTag = 0,
30
  kNullTag = 0,
31
32
  /**
33
   * The lower type ids are reserved for the embedder to assign. For that, the
34
   * main requirement is that all (transitive) child classes of a given parent
35
   * class have type ids in the same range, and that there are no unrelated
36
   * types in that range. For example, given the following type hierarchy:
37
   *
38
   *          A     F
39
   *         / \
40
   *        B   E
41
   *       / \
42
   *      C   D
43
   *
44
   * a potential type id assignment that satistifes these requirements is
45
   * {C: 0, D: 1, B: 2, A: 3, E: 4, F: 5}. With that, the type check for type A
46
   * would check for the range [0, 4], while the check for B would check range
47
   * [0, 2], and for F it would simply check [5, 5].
48
   *
49
   * In addition, there is an option for performance tweaks: if the size of the
50
   * type range corresponding to a supertype is a power of two and starts at a
51
   * power of two (e.g. [0x100, 0x13f]), then the compiler can often optimize
52
   * the type check to use even fewer instructions (essentially replace a AND +
53
   * SUB with a single AND).
54
   */
55
56
  kDefaultTag = 0x7000,
57
58
  kZappedEntryTag = 0x7ffd,
59
  kEvacuationEntryTag = 0x7ffe,
60
  kFreeEntryTag = 0x7fff,
61
  // The tags are limited to 15 bits, so the last tag is 0x7fff.
62
  kLastTag = 0x7fff,
63
};
64
65
// Convenience struct to represent tag ranges. This is used for type checks
66
// against supertypes, which cover a range of types (their subtypes).
67
// Both the lower- and the upper bound are inclusive. In other words, this
68
// struct represents the range [lower_bound, upper_bound].
69
// TODO(saelo): reuse internal::TagRange here.
70
struct CppHeapPointerTagRange {
71
  constexpr CppHeapPointerTagRange(CppHeapPointerTag lower,
72
                                   CppHeapPointerTag upper)
73
0
      : lower_bound(lower), upper_bound(upper) {}
74
  CppHeapPointerTag lower_bound;
75
  CppHeapPointerTag upper_bound;
76
77
  // Check whether the tag of the given CppHeapPointerTable entry is within
78
  // this range. This method encodes implementation details of the
79
  // CppHeapPointerTable, which is necessary as it is used by
80
  // ReadCppHeapPointerField below.
81
  // Returns true if the check is successful and the tag of the given entry is
82
  // within this range, false otherwise.
83
0
  bool CheckTagOf(uint64_t entry) {
84
0
    // Note: the cast to uint32_t is important here. Otherwise, the uint16_t's
85
0
    // would be promoted to int in the range check below, which would result in
86
0
    // undefined behavior (signed integer undeflow) if the actual value is less
87
0
    // than the lower bound. Then, the compiler would take advantage of the
88
0
    // undefined behavior and turn the range check into a simple
89
0
    // `actual_tag <= last_tag` comparison, which is incorrect.
90
0
    uint32_t actual_tag = static_cast<uint16_t>(entry);
91
0
    // The actual_tag is shifted to the left by one and contains the marking
92
0
    // bit in the LSB. To ignore that during the type check, simply add one to
93
0
    // the (shifted) range.
94
0
    constexpr int kTagShift = internal::kCppHeapPointerTagShift;
95
0
    uint32_t first_tag = static_cast<uint32_t>(lower_bound) << kTagShift;
96
0
    uint32_t last_tag = (static_cast<uint32_t>(upper_bound) << kTagShift) + 1;
97
0
    return actual_tag >= first_tag && actual_tag <= last_tag;
98
0
  }
99
};
100
101
constexpr CppHeapPointerTagRange kAnyCppHeapPointer(
102
    CppHeapPointerTag::kFirstTag, CppHeapPointerTag::kLastTag);
103
104
/**
105
 * Hardware support for the V8 Sandbox.
106
 *
107
 * This is an experimental feature that may change or be removed without
108
 * further notice. Use at your own risk.
109
 */
110
class SandboxHardwareSupport {
111
 public:
112
  /**
113
   * Initialize sandbox hardware support. This needs to be called before
114
   * creating any thread that might access sandbox memory since it sets up
115
   * hardware permissions to the memory that will be inherited on clone.
116
   */
117
  V8_EXPORT static void InitializeBeforeThreadCreation();
118
119
  /**
120
   * Prepares the current thread for executing sandboxed code.
121
   *
122
   * This must be called on newly created threads before they execute any
123
   * sandboxed code (in particular any JavaScript or WebAssembly code). It
124
   * should not be invoked on threads that never execute sandboxed code,
125
   * although it is fine to do so from a security point of view.
126
   */
127
  V8_EXPORT static void PrepareCurrentThreadForHardwareSandboxing();
128
};
129
130
namespace internal {
131
132
#ifdef V8_COMPRESS_POINTERS
133
V8_INLINE static Address* GetCppHeapPointerTableBase(v8::Isolate* isolate) {
134
  Address addr = reinterpret_cast<Address>(isolate) +
135
                 Internals::kIsolateCppHeapPointerTableOffset +
136
                 Internals::kExternalPointerTableBasePointerOffset;
137
  return *reinterpret_cast<Address**>(addr);
138
}
139
#endif  // V8_COMPRESS_POINTERS
140
141
template <typename T>
142
V8_INLINE static T* ReadCppHeapPointerField(v8::Isolate* isolate,
143
                                            Address heap_object_ptr, int offset,
144
                                            CppHeapPointerTagRange tag_range) {
145
#ifdef V8_COMPRESS_POINTERS
146
  // See src/sandbox/cppheap-pointer-table-inl.h. Logic duplicated here so
147
  // it can be inlined and doesn't require an additional call.
148
  const CppHeapPointerHandle handle =
149
      Internals::ReadRawField<CppHeapPointerHandle>(heap_object_ptr, offset);
150
  const uint32_t index = handle >> kExternalPointerIndexShift;
151
  const Address* table = GetCppHeapPointerTableBase(isolate);
152
  const std::atomic<Address>* ptr =
153
      reinterpret_cast<const std::atomic<Address>*>(&table[index]);
154
  Address entry = std::atomic_load_explicit(ptr, std::memory_order_relaxed);
155
156
  Address pointer = entry;
157
  if (V8_LIKELY(tag_range.CheckTagOf(entry))) {
158
    pointer = entry >> kCppHeapPointerPayloadShift;
159
  } else {
160
    // If the type check failed, we simply return nullptr here. That way:
161
    //  1. The null handle always results in nullptr being returned here, which
162
    //     is a desired property. Otherwise, we would need an explicit check for
163
    //     the null handle above, and therefore an additional branch. This
164
    //     works because the 0th entry of the table always contains nullptr
165
    //     tagged with the null tag (i.e. an all-zeros entry). As such,
166
    //     regardless of whether the type check succeeds, the result will
167
    //     always be nullptr.
168
    //  2. The returned pointer is guaranteed to crash even on platforms with
169
    //     top byte ignore (TBI), such as Arm64. The alternative would be to
170
    //     simply return the original entry with the left-shifted payload.
171
    //     However, due to TBI, an access to that may not always result in a
172
    //     crash (specifically, if the second most significant byte happens to
173
    //     be zero). In addition, there shouldn't be a difference on Arm64
174
    //     between returning nullptr or the original entry, since it will
175
    //     simply compile to a `csel x0, x8, xzr, lo` instead of a
176
    //     `csel x0, x10, x8, lo` instruction.
177
    pointer = 0;
178
  }
179
  return reinterpret_cast<T*>(pointer);
180
#else   // !V8_COMPRESS_POINTERS
181
  return reinterpret_cast<T*>(
182
      Internals::ReadRawField<Address>(heap_object_ptr, offset));
183
#endif  // !V8_COMPRESS_POINTERS
184
}
185
186
}  // namespace internal
187
}  // namespace v8
188
189
#endif  // INCLUDE_V8_SANDBOX_H_