mirror of
https://github.com/nodejs/node.git
synced 2026-10-28 10:15:07 +08:00
src: support building with the V8 sandbox
With V8_ENABLE_SANDBOX every ArrayBuffer backing store has to be allocated inside the sandbox, so memory that Node.js or a library allocated itself cannot be wrapped and has to be copied in. Several places already special-cased this, each with its own #ifdef, but a build with the sandbox enabled still failed to compile (`kMaxSafeBufferSizeForSandbox`), aborted in `crypto.randomUUID()` (`SecureBuffer` was UNREACHABLE) and threw from every caller of the malloc-owning `Buffer::New()` (v8.serialize, string transcoding, IPC, the public `Buffer::New(isolate, data, length)`), and trace_events, SEA assets and FFI still wrapped outside memory unconditionally. Add `AdoptIntoBackingStore()`, which wraps the memory as before in regular builds and copies it into an isolate-allocated backing store, running the deleter right away, when the sandbox is enabled, and use it at all of those sites so the #ifdef lives in one place. The trace category flag cannot be copied since JS polls it, so under the sandbox `getCategoryEnabledBuffer()` returns nothing and lib falls back to `isTraceCategoryEnabled()`; FFI zero-copy views throw ERR_OPERATION_FAILED there for the same reason. Signed-off-by: Shelley Vohr <shelley.vohr@gmail.com> PR-URL: https://github.com/nodejs/node/pull/62237 Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: Chengzhong Wu <legendecas@gmail.com>
This commit is contained in:
24 files changed
+222
-183
No files matched your search
+4
-5
@@ -256,11 +256,10 @@ function exportString(str, data, len, encoding = 'utf8') {
|
||||
throw new ERR_OUT_OF_RANGE('len', `>= ${requiredLength}`, len);
|
||||
}
|
||||
|
||||
const targetBuffer = toBuffer(data, len, false);
|
||||
const dataLength = sourceBuffer.length;
|
||||
|
||||
sourceBuffer.copy(targetBuffer, 0, 0, dataLength);
|
||||
targetBuffer.fill(0, dataLength, dataLength + terminatorSize);
|
||||
const terminated = Buffer.allocUnsafe(requiredLength);
|
||||
sourceBuffer.copy(terminated);
|
||||
terminated.fill(0, sourceBuffer.length);
|
||||
exportBytes(terminated, data, len);
|
||||
}
|
||||
|
||||
function exportBuffer(source, data, len) {
|
||||
|
||||
@@ -10,7 +10,7 @@ const {
|
||||
|
||||
const { setUnrefTimeout } = require('internal/timers');
|
||||
const {
|
||||
getCategoryEnabledBuffer,
|
||||
categoryEnabledChecker,
|
||||
trace,
|
||||
nodeTraceEventCategory,
|
||||
kAsyncBegin,
|
||||
@@ -44,11 +44,7 @@ function getNextTraceEventId() {
|
||||
return ++traceEventId;
|
||||
}
|
||||
|
||||
const httpEnabled = getCategoryEnabledBuffer('node.http');
|
||||
|
||||
function isTraceHTTPEnabled() {
|
||||
return httpEnabled[0] > 0;
|
||||
}
|
||||
const isTraceHTTPEnabled = categoryEnabledChecker('node.http');
|
||||
|
||||
const traceEventCategory = nodeTraceEventCategory('node.http');
|
||||
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
'use strict';
|
||||
|
||||
const { getCategoryEnabledBuffer, trace, usePerfetto } = internalBinding('trace_events');
|
||||
const {
|
||||
getCategoryEnabledBuffer,
|
||||
isTraceCategoryEnabled,
|
||||
trace,
|
||||
usePerfetto,
|
||||
} = internalBinding('trace_events');
|
||||
const {
|
||||
CHAR_UPPERCASE_B,
|
||||
CHAR_LOWERCASE_B,
|
||||
@@ -17,6 +22,16 @@ if (usePerfetto) {
|
||||
nodeTraceEventCategory = (category) => `node,${category}`;
|
||||
}
|
||||
|
||||
// The returned function reads the category's enabled flag directly when the
|
||||
// binding can expose it to JS, and asks the tracing controller otherwise.
|
||||
function categoryEnabledChecker(category) {
|
||||
const buffer = getCategoryEnabledBuffer(category);
|
||||
if (buffer === undefined) {
|
||||
return () => isTraceCategoryEnabled(category);
|
||||
}
|
||||
return () => buffer[0] > 0;
|
||||
}
|
||||
|
||||
// The async events describe the execution of a single asynchronous operation, and are
|
||||
// used to measure the time spent in a single asynchronous operation.
|
||||
// Async events may overlap with each other. Different events do not have
|
||||
@@ -44,7 +59,7 @@ const kTraceInstant = CHAR_LOWERCASE_N;
|
||||
|
||||
module.exports = {
|
||||
usePerfetto,
|
||||
getCategoryEnabledBuffer,
|
||||
categoryEnabledChecker,
|
||||
trace,
|
||||
nodeTraceEventCategory,
|
||||
kAsyncBegin,
|
||||
|
||||
@@ -16,7 +16,7 @@ const {
|
||||
} = primordials;
|
||||
const { inspect, format, formatWithOptions } = require('internal/util/inspect');
|
||||
const {
|
||||
getCategoryEnabledBuffer,
|
||||
categoryEnabledChecker,
|
||||
trace,
|
||||
nodeTraceEventCategory,
|
||||
kAsyncBegin,
|
||||
@@ -388,14 +388,14 @@ function debugWithTimer(set, cb) {
|
||||
}
|
||||
|
||||
const traceCategory = nodeTraceEventCategory(`node.${StringPrototypeToLowerCase(set)}`);
|
||||
let traceCategoryBuffer;
|
||||
let traceCategoryEnabled;
|
||||
let debugLogCategoryEnabled = false;
|
||||
let timerFlags = kNone;
|
||||
|
||||
function ensureTimerFlagsAreUpdated() {
|
||||
timerFlags &= ~kSkipTrace;
|
||||
|
||||
if (traceCategoryBuffer[0] === 0) {
|
||||
if (!traceCategoryEnabled()) {
|
||||
timerFlags |= kSkipTrace;
|
||||
}
|
||||
}
|
||||
@@ -469,7 +469,7 @@ function debugWithTimer(set, cb) {
|
||||
}
|
||||
emitWarningIfNeeded(set);
|
||||
debugLogCategoryEnabled = testEnabled(set);
|
||||
traceCategoryBuffer = getCategoryEnabledBuffer(traceCategory);
|
||||
traceCategoryEnabled = categoryEnabledChecker(traceCategory);
|
||||
|
||||
timerFlags = kNone;
|
||||
|
||||
@@ -477,7 +477,7 @@ function debugWithTimer(set, cb) {
|
||||
timerFlags |= kSkipLog;
|
||||
}
|
||||
|
||||
if (traceCategoryBuffer[0] === 0) {
|
||||
if (!traceCategoryEnabled()) {
|
||||
timerFlags |= kSkipTrace;
|
||||
}
|
||||
|
||||
|
||||
+5
-17
@@ -22,8 +22,6 @@ using ncrypto::DHPointer;
|
||||
using ncrypto::EVPKeyCtxPointer;
|
||||
using ncrypto::EVPKeyPointer;
|
||||
using v8::ArrayBuffer;
|
||||
using v8::BackingStoreInitializationMode;
|
||||
using v8::BackingStoreOnFailureMode;
|
||||
using v8::ConstructorBehavior;
|
||||
using v8::Context;
|
||||
using v8::DontDelete;
|
||||
@@ -60,21 +58,8 @@ MaybeLocal<Value> DataPointerToBuffer(Environment* env, DataPointer&& data) {
|
||||
struct Flag {
|
||||
bool secure;
|
||||
};
|
||||
#ifdef V8_ENABLE_SANDBOX
|
||||
auto backing = ArrayBuffer::NewBackingStore(
|
||||
auto backing = AdoptIntoBackingStore(
|
||||
env->isolate(),
|
||||
data.size(),
|
||||
BackingStoreInitializationMode::kUninitialized,
|
||||
BackingStoreOnFailureMode::kReturnNull);
|
||||
if (!backing) {
|
||||
THROW_ERR_MEMORY_ALLOCATION_FAILED(env);
|
||||
return MaybeLocal<Value>();
|
||||
}
|
||||
if (data.size() > 0) {
|
||||
memcpy(backing->Data(), data.get(), data.size());
|
||||
}
|
||||
#else
|
||||
auto backing = ArrayBuffer::NewBackingStore(
|
||||
data.get(),
|
||||
data.size(),
|
||||
[](void* data, size_t len, void* ptr) {
|
||||
@@ -83,7 +68,10 @@ MaybeLocal<Value> DataPointerToBuffer(Environment* env, DataPointer&& data) {
|
||||
},
|
||||
new Flag{data.isSecure()});
|
||||
data.release();
|
||||
#endif // V8_ENABLE_SANDBOX
|
||||
if (!backing) {
|
||||
THROW_ERR_MEMORY_ALLOCATION_FAILED(env);
|
||||
return MaybeLocal<Value>();
|
||||
}
|
||||
|
||||
auto ab = ArrayBuffer::New(env->isolate(), std::move(backing));
|
||||
return Buffer::New(env, ab, 0, ab->ByteLength()).FromMaybe(Local<Value>());
|
||||
|
||||
+13
-39
@@ -46,8 +46,6 @@ using v8::Array;
|
||||
using v8::ArrayBuffer;
|
||||
using v8::ArrayBufferView;
|
||||
using v8::BackingStore;
|
||||
using v8::BackingStoreInitializationMode;
|
||||
using v8::BackingStoreOnFailureMode;
|
||||
using v8::BigInt;
|
||||
using v8::Context;
|
||||
using v8::DictionaryTemplate;
|
||||
@@ -802,36 +800,18 @@ std::unique_ptr<BackingStore> ByteSource::ReleaseToBackingStore(
|
||||
// It's ok for allocated_data_ to be nullptr but
|
||||
// only if size_ is zero.
|
||||
CHECK_IMPLIES(size_ > 0, allocated_data_ != nullptr);
|
||||
#ifdef V8_ENABLE_SANDBOX
|
||||
// If the v8 sandbox is enabled, then all array buffers must be allocated
|
||||
// via the isolate. External buffers are not allowed. So, instead of wrapping
|
||||
// the allocated data we'll copy it instead.
|
||||
|
||||
// TODO(@jasnell): It would be nice to use an abstracted utility to do this
|
||||
// branch instead of duplicating the V8_ENABLE_SANDBOX check each time.
|
||||
std::unique_ptr<BackingStore> ptr = ArrayBuffer::NewBackingStore(
|
||||
std::unique_ptr<BackingStore> ptr = AdoptIntoBackingStore(
|
||||
env->isolate(),
|
||||
size(),
|
||||
BackingStoreInitializationMode::kUninitialized,
|
||||
BackingStoreOnFailureMode::kReturnNull);
|
||||
if (!ptr) {
|
||||
THROW_ERR_MEMORY_ALLOCATION_FAILED(env);
|
||||
return nullptr;
|
||||
}
|
||||
memcpy(ptr->Data(), allocated_data_, size());
|
||||
OPENSSL_clear_free(allocated_data_, size_);
|
||||
#else
|
||||
std::unique_ptr<BackingStore> ptr = ArrayBuffer::NewBackingStore(
|
||||
allocated_data_,
|
||||
size(),
|
||||
[](void* data, size_t length, void* deleter_data) {
|
||||
OPENSSL_clear_free(deleter_data, length);
|
||||
}, allocated_data_);
|
||||
#endif // V8_ENABLE_SANDBOX
|
||||
CHECK(ptr);
|
||||
[](void* data, size_t length, void*) {
|
||||
OPENSSL_clear_free(data, length);
|
||||
},
|
||||
nullptr);
|
||||
allocated_data_ = nullptr;
|
||||
data_ = nullptr;
|
||||
size_ = 0;
|
||||
if (!ptr) THROW_ERR_MEMORY_ALLOCATION_FAILED(env);
|
||||
return ptr;
|
||||
}
|
||||
|
||||
@@ -1193,17 +1173,6 @@ namespace {
|
||||
// initialized, SecureBuffer will automatically use it.
|
||||
void SecureBuffer(const FunctionCallbackInfo<Value>& args) {
|
||||
Environment* env = Environment::GetCurrent(args);
|
||||
#ifdef V8_ENABLE_SANDBOX
|
||||
// The v8 sandbox is enabled, so we cannot use the secure heap because
|
||||
// the sandbox requires that all array buffers be allocated via the isolate.
|
||||
// That is fundamentally incompatible with the secure heap which allocates
|
||||
// in openssl's secure heap area. Instead we'll just throw an error here.
|
||||
//
|
||||
// That said, we really shouldn't get here in the first place since the
|
||||
// option to enable the secure heap is only available when the sandbox
|
||||
// is disabled.
|
||||
UNREACHABLE();
|
||||
#else
|
||||
CHECK(args[0]->IsUint32());
|
||||
uint32_t len = args[0].As<Uint32>()->Value();
|
||||
|
||||
@@ -1213,7 +1182,10 @@ void SecureBuffer(const FunctionCallbackInfo<Value>& args) {
|
||||
}
|
||||
auto released = data.release();
|
||||
|
||||
std::shared_ptr<BackingStore> store = ArrayBuffer::NewBackingStore(
|
||||
// Under V8_ENABLE_SANDBOX this ends up as a plain copy, which is fine:
|
||||
// --secure-heap is unavailable there, so SecureAlloc() is OPENSSL_malloc().
|
||||
std::shared_ptr<BackingStore> store = AdoptIntoBackingStore(
|
||||
env->isolate(),
|
||||
released.data,
|
||||
released.len,
|
||||
[](void* data, size_t len, void* deleter_data) {
|
||||
@@ -1227,10 +1199,12 @@ void SecureBuffer(const FunctionCallbackInfo<Value>& args) {
|
||||
true);
|
||||
},
|
||||
nullptr);
|
||||
if (!store) {
|
||||
return THROW_ERR_MEMORY_ALLOCATION_FAILED(env);
|
||||
}
|
||||
|
||||
Local<ArrayBuffer> buffer = ArrayBuffer::New(env->isolate(), store);
|
||||
args.GetReturnValue().Set(Uint8Array::New(buffer, 0, len));
|
||||
#endif // V8_ENABLE_SANDBOX
|
||||
}
|
||||
|
||||
void SecureHeapUsed(const FunctionCallbackInfo<Value>& args) {
|
||||
|
||||
@@ -27,8 +27,6 @@ using ncrypto::X509View;
|
||||
using v8::Array;
|
||||
using v8::ArrayBuffer;
|
||||
using v8::ArrayBufferView;
|
||||
using v8::BackingStoreInitializationMode;
|
||||
using v8::BackingStoreOnFailureMode;
|
||||
using v8::Boolean;
|
||||
using v8::Context;
|
||||
using v8::Date;
|
||||
@@ -140,29 +138,18 @@ MaybeLocal<Value> ToBuffer(Environment* env, BIOPointer* bio) {
|
||||
BUF_MEM* mem = *bio;
|
||||
if (!mem) [[unlikely]]
|
||||
return {};
|
||||
#ifdef V8_ENABLE_SANDBOX
|
||||
// If the v8 sandbox is enabled, then all array buffers must be allocated
|
||||
// via the isolate. External buffers are not allowed. So, instead of wrapping
|
||||
// the BIOPointer we'll copy it instead.
|
||||
auto backing = ArrayBuffer::NewBackingStore(
|
||||
auto backing = AdoptIntoBackingStore(
|
||||
env->isolate(),
|
||||
mem->length,
|
||||
BackingStoreInitializationMode::kUninitialized,
|
||||
BackingStoreOnFailureMode::kReturnNull);
|
||||
if (!backing) {
|
||||
THROW_ERR_MEMORY_ALLOCATION_FAILED(env);
|
||||
return MaybeLocal<Value>();
|
||||
}
|
||||
memcpy(backing->Data(), mem->data, mem->length);
|
||||
#else
|
||||
auto backing = ArrayBuffer::NewBackingStore(
|
||||
mem->data,
|
||||
mem->length,
|
||||
[](void*, size_t, void* data) {
|
||||
BIOPointer free_me(static_cast<BIO*>(data));
|
||||
},
|
||||
bio->release());
|
||||
#endif // V8_ENABLE_SANDBOX
|
||||
if (!backing) {
|
||||
THROW_ERR_MEMORY_ALLOCATION_FAILED(env);
|
||||
return MaybeLocal<Value>();
|
||||
}
|
||||
auto ab = ArrayBuffer::New(env->isolate(), std::move(backing));
|
||||
Local<Value> ret;
|
||||
if (!Buffer::New(env, ab, 0, ab->ByteLength()).ToLocal(&ret)) return {};
|
||||
|
||||
+21
-5
@@ -533,6 +533,17 @@ void ToString(const FunctionCallbackInfo<Value>& args) {
|
||||
args.GetReturnValue().Set(out);
|
||||
}
|
||||
|
||||
// Foreign memory lies outside the V8 sandbox and cannot back an ArrayBuffer.
|
||||
static bool ZeroCopyUnavailable(Environment* env) {
|
||||
#ifdef V8_ENABLE_SANDBOX
|
||||
THROW_ERR_OPERATION_FAILED(
|
||||
env, "Zero-copy views are not available when the V8 sandbox is enabled");
|
||||
return true;
|
||||
#else
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
void ToBuffer(const FunctionCallbackInfo<Value>& args) {
|
||||
Environment* env = Environment::GetCurrent(args);
|
||||
Isolate* isolate = env->isolate();
|
||||
@@ -578,9 +589,12 @@ void ToBuffer(const FunctionCallbackInfo<Value>& args) {
|
||||
return;
|
||||
}
|
||||
|
||||
bool copy = args.Length() < 3 || args[2]->IsUndefined() ||
|
||||
args[2]->BooleanValue(isolate);
|
||||
if (!copy && ZeroCopyUnavailable(env)) return;
|
||||
|
||||
Local<Object> buf;
|
||||
if (args.Length() < 3 || args[2]->IsUndefined() ||
|
||||
args[2]->BooleanValue(isolate)) {
|
||||
if (copy) {
|
||||
if (!Buffer::Copy(env, reinterpret_cast<char*>(ptr), len).ToLocal(&buf)) {
|
||||
return;
|
||||
}
|
||||
@@ -640,10 +654,12 @@ void ToArrayBuffer(const FunctionCallbackInfo<Value>& args) {
|
||||
return;
|
||||
}
|
||||
|
||||
Local<ArrayBuffer> ab;
|
||||
bool copy = args.Length() < 3 || args[2]->IsUndefined() ||
|
||||
args[2]->BooleanValue(isolate);
|
||||
if (!copy && ZeroCopyUnavailable(env)) return;
|
||||
|
||||
if (args.Length() < 3 || args[2]->IsUndefined() ||
|
||||
args[2]->BooleanValue(isolate)) {
|
||||
Local<ArrayBuffer> ab;
|
||||
if (copy) {
|
||||
std::unique_ptr<BackingStore> store =
|
||||
ArrayBuffer::NewBackingStore(isolate, len);
|
||||
memcpy(store->Data(), reinterpret_cast<void*>(ptr), len);
|
||||
|
||||
+31
-34
@@ -88,7 +88,7 @@ namespace {
|
||||
|
||||
class CallbackInfo : public Cleanable {
|
||||
public:
|
||||
static inline Local<ArrayBuffer> CreateTrackedArrayBuffer(
|
||||
static inline MaybeLocal<ArrayBuffer> CreateTrackedArrayBuffer(
|
||||
Environment* env,
|
||||
char* data,
|
||||
size_t length,
|
||||
@@ -114,7 +114,7 @@ class CallbackInfo : public Cleanable {
|
||||
Environment* const env_;
|
||||
};
|
||||
|
||||
Local<ArrayBuffer> CallbackInfo::CreateTrackedArrayBuffer(
|
||||
MaybeLocal<ArrayBuffer> CallbackInfo::CreateTrackedArrayBuffer(
|
||||
Environment* env,
|
||||
char* data,
|
||||
size_t length,
|
||||
@@ -124,10 +124,18 @@ Local<ArrayBuffer> CallbackInfo::CreateTrackedArrayBuffer(
|
||||
CHECK_IMPLIES(data == nullptr, length == 0);
|
||||
|
||||
CallbackInfo* self = new CallbackInfo(env, callback, data, hint);
|
||||
std::unique_ptr<BackingStore> bs =
|
||||
ArrayBuffer::NewBackingStore(data, length, [](void*, size_t, void* arg) {
|
||||
std::unique_ptr<BackingStore> bs = AdoptIntoBackingStore(
|
||||
env->isolate(),
|
||||
data,
|
||||
length,
|
||||
[](void*, size_t, void* arg) {
|
||||
static_cast<CallbackInfo*>(arg)->OnBackingStoreFree();
|
||||
}, self);
|
||||
},
|
||||
self);
|
||||
if (!bs) {
|
||||
THROW_ERR_MEMORY_ALLOCATION_FAILED(env);
|
||||
return MaybeLocal<ArrayBuffer>();
|
||||
}
|
||||
Local<ArrayBuffer> ab = ArrayBuffer::New(env->isolate(), std::move(bs));
|
||||
|
||||
// V8 simply ignores the BackingStore deleter callback if data == nullptr,
|
||||
@@ -135,7 +143,7 @@ Local<ArrayBuffer> CallbackInfo::CreateTrackedArrayBuffer(
|
||||
if (data == nullptr) {
|
||||
ab->Detach(Local<Value>()).Check();
|
||||
self->OnBackingStoreFree(); // This calls `callback` asynchronously.
|
||||
} else {
|
||||
} else if (ab->Data() == data) {
|
||||
// Store the ArrayBuffer so that we can detach it later.
|
||||
self->persistent_.Reset(env->isolate(), ab);
|
||||
self->persistent_.SetWeak();
|
||||
@@ -144,7 +152,6 @@ Local<ArrayBuffer> CallbackInfo::CreateTrackedArrayBuffer(
|
||||
return ab;
|
||||
}
|
||||
|
||||
|
||||
CallbackInfo::CallbackInfo(Environment* env,
|
||||
FreeCallback callback,
|
||||
char* data,
|
||||
@@ -481,11 +488,13 @@ MaybeLocal<Object> New(Environment* env,
|
||||
return Local<Object>();
|
||||
}
|
||||
|
||||
Local<ArrayBuffer> ab =
|
||||
CallbackInfo::CreateTrackedArrayBuffer(env, data, length, callback, hint);
|
||||
if (ab->SetPrivate(env->context(),
|
||||
Local<ArrayBuffer> ab;
|
||||
if (!CallbackInfo::CreateTrackedArrayBuffer(env, data, length, callback, hint)
|
||||
.ToLocal(&ab) ||
|
||||
ab->SetPrivate(env->context(),
|
||||
env->untransferable_object_private_symbol(),
|
||||
True(env->isolate())).IsNothing()) {
|
||||
True(env->isolate()))
|
||||
.IsNothing()) {
|
||||
return Local<Object>();
|
||||
}
|
||||
MaybeLocal<Uint8Array> maybe_ui = Buffer::New(env, ab, 0, length);
|
||||
@@ -529,32 +538,24 @@ MaybeLocal<Object> New(Environment* env,
|
||||
}
|
||||
}
|
||||
|
||||
#if defined(V8_ENABLE_SANDBOX)
|
||||
// When v8 sandbox is enabled, external backing stores are not supported
|
||||
// since all arraybuffer allocations are expected to be done by the isolate.
|
||||
// Since this violates the contract of this function, let's free the data and
|
||||
// throw an error.
|
||||
free(data);
|
||||
THROW_ERR_OPERATION_FAILED(
|
||||
env->isolate(),
|
||||
"Wrapping external data is not supported when the v8 sandbox is enabled");
|
||||
return MaybeLocal<Object>();
|
||||
#else
|
||||
EscapableHandleScope handle_scope(env->isolate());
|
||||
|
||||
auto free_callback = [](void* data, size_t length, void* deleter_data) {
|
||||
free(data);
|
||||
};
|
||||
std::unique_ptr<BackingStore> bs =
|
||||
ArrayBuffer::NewBackingStore(data, length, free_callback, nullptr);
|
||||
|
||||
std::unique_ptr<BackingStore> bs = AdoptIntoBackingStore(
|
||||
env->isolate(),
|
||||
data,
|
||||
length,
|
||||
[](void* data, size_t, void*) { free(data); },
|
||||
nullptr);
|
||||
if (!bs) {
|
||||
THROW_ERR_MEMORY_ALLOCATION_FAILED(env);
|
||||
return MaybeLocal<Object>();
|
||||
}
|
||||
Local<ArrayBuffer> ab = ArrayBuffer::New(env->isolate(), std::move(bs));
|
||||
|
||||
Local<Object> obj;
|
||||
if (Buffer::New(env, ab, 0, length).ToLocal(&obj))
|
||||
return handle_scope.Escape(obj);
|
||||
return Local<Object>();
|
||||
#endif
|
||||
}
|
||||
|
||||
namespace {
|
||||
@@ -1608,11 +1609,7 @@ inline size_t CheckNumberToSize(Local<Value> number) {
|
||||
// See v8::internal::TryNumberToSize on this (and on < comparison)
|
||||
double maxSize = static_cast<double>(std::numeric_limits<size_t>::max());
|
||||
CHECK(value >= 0 && value < maxSize);
|
||||
size_t size = static_cast<size_t>(value);
|
||||
#ifdef V8_ENABLE_SANDBOX
|
||||
CHECK_LE(size, kMaxSafeBufferSizeForSandbox);
|
||||
#endif
|
||||
return size;
|
||||
return static_cast<size_t>(value);
|
||||
}
|
||||
|
||||
// Allocates an ArrayBuffer of `size` bytes. Its contents are left
|
||||
|
||||
+4
-2
@@ -53,14 +53,16 @@ NODE_EXTERN v8::MaybeLocal<v8::Object> New(v8::Isolate* isolate,
|
||||
v8::Local<v8::String> string,
|
||||
enum encoding enc = UTF8);
|
||||
|
||||
// public constructor - data is used, callback is passed data on object gc
|
||||
// public constructor - data is used, callback is passed data on object gc.
|
||||
// If V8 is built with its sandbox, data is copied instead and callback runs
|
||||
// on the next turn of the event loop.
|
||||
NODE_EXTERN v8::MaybeLocal<v8::Object> New(v8::Isolate* isolate,
|
||||
char* data,
|
||||
size_t length,
|
||||
FreeCallback callback,
|
||||
void* hint);
|
||||
|
||||
// public constructor - data is used.
|
||||
// public constructor - data is used, or copied and freed under the V8 sandbox.
|
||||
NODE_EXTERN v8::MaybeLocal<v8::Object> New(v8::Isolate* isolate,
|
||||
char* data,
|
||||
size_t len);
|
||||
|
||||
+3
-1
@@ -878,11 +878,13 @@ void GetAsset(const FunctionCallbackInfo<Value>& args) {
|
||||
}
|
||||
// We cast away the constness here, the JS land should ensure that
|
||||
// the data is not mutated.
|
||||
std::unique_ptr<v8::BackingStore> store = ArrayBuffer::NewBackingStore(
|
||||
std::unique_ptr<v8::BackingStore> store = AdoptIntoBackingStore(
|
||||
args.GetIsolate(),
|
||||
const_cast<char*>(it->second.data()),
|
||||
it->second.size(),
|
||||
[](void*, size_t, void*) {},
|
||||
nullptr);
|
||||
CHECK(store);
|
||||
Local<ArrayBuffer> ab = ArrayBuffer::New(args.GetIsolate(), std::move(store));
|
||||
args.GetReturnValue().Set(ab);
|
||||
}
|
||||
|
||||
+4
-14
@@ -2034,27 +2034,17 @@ void DatabaseSync::Serialize(const FunctionCallbackInfo<Value>& args) {
|
||||
return;
|
||||
}
|
||||
|
||||
// V8 sandbox forbids external backing stores so allocate inside the
|
||||
// sandbox and copy. Without sandbox wrap the output directly using
|
||||
// sqlite3_free as the destructor to avoid the copy.
|
||||
#ifdef V8_ENABLE_SANDBOX
|
||||
auto free_data = OnScopeLeave([&] { sqlite3_free(data); });
|
||||
auto store = ArrayBuffer::NewBackingStore(
|
||||
auto store = AdoptIntoBackingStore(
|
||||
env->isolate(),
|
||||
data,
|
||||
size,
|
||||
BackingStoreInitializationMode::kUninitialized,
|
||||
BackingStoreOnFailureMode::kReturnNull);
|
||||
[](void* ptr, size_t, void*) { sqlite3_free(ptr); },
|
||||
nullptr);
|
||||
if (!store) {
|
||||
THROW_ERR_MEMORY_ALLOCATION_FAILED(env);
|
||||
return;
|
||||
}
|
||||
memcpy(store->Data(), data, size);
|
||||
Local<ArrayBuffer> ab = ArrayBuffer::New(env->isolate(), std::move(store));
|
||||
#else
|
||||
auto store = ArrayBuffer::NewBackingStore(
|
||||
data, size, [](void* ptr, size_t, void*) { sqlite3_free(ptr); }, nullptr);
|
||||
Local<ArrayBuffer> ab = ArrayBuffer::New(env->isolate(), std::move(store));
|
||||
#endif
|
||||
|
||||
args.GetReturnValue().Set(Uint8Array::New(ab, 0, size));
|
||||
}
|
||||
|
||||
@@ -133,7 +133,9 @@ static void SetTraceCategoryStateUpdateHandler(
|
||||
|
||||
static void GetCategoryEnabledBuffer(const FunctionCallbackInfo<Value>& args) {
|
||||
CHECK(args[0]->IsString());
|
||||
|
||||
// The flag lives outside the V8 sandbox and cannot back an ArrayBuffer
|
||||
// there; lib/internal/trace_events.js falls back to isTraceCategoryEnabled().
|
||||
#ifndef V8_ENABLE_SANDBOX
|
||||
Isolate* isolate = args.GetIsolate();
|
||||
node::Utf8Value category_name(isolate, args[0]);
|
||||
|
||||
@@ -150,6 +152,7 @@ static void GetCategoryEnabledBuffer(const FunctionCallbackInfo<Value>& args) {
|
||||
v8::Local<Uint8Array> u8 = v8::Uint8Array::New(ab, 0, 1);
|
||||
|
||||
args.GetReturnValue().Set(u8);
|
||||
#endif
|
||||
}
|
||||
|
||||
void NodeCategorySet::Initialize(Local<Object> target,
|
||||
|
||||
+21
@@ -87,6 +87,7 @@ namespace node {
|
||||
|
||||
using v8::ArrayBuffer;
|
||||
using v8::ArrayBufferView;
|
||||
using v8::BackingStore;
|
||||
using v8::Context;
|
||||
using v8::FunctionTemplate;
|
||||
using v8::Isolate;
|
||||
@@ -677,6 +678,26 @@ Local<String> UnionBytes::ToStringChecked(Isolate* isolate) const {
|
||||
}
|
||||
}
|
||||
|
||||
std::unique_ptr<BackingStore> AdoptIntoBackingStore(
|
||||
Isolate* isolate,
|
||||
void* data,
|
||||
size_t byte_length,
|
||||
BackingStore::DeleterCallback deleter,
|
||||
void* deleter_data) {
|
||||
#ifdef V8_ENABLE_SANDBOX
|
||||
std::unique_ptr<BackingStore> store = ArrayBuffer::NewBackingStore(
|
||||
isolate,
|
||||
byte_length,
|
||||
v8::BackingStoreInitializationMode::kUninitialized,
|
||||
v8::BackingStoreOnFailureMode::kReturnNull);
|
||||
if (store && byte_length > 0) memcpy(store->Data(), data, byte_length);
|
||||
if (data != nullptr) deleter(data, byte_length, deleter_data);
|
||||
return store;
|
||||
#else
|
||||
return ArrayBuffer::NewBackingStore(data, byte_length, deleter, deleter_data);
|
||||
#endif
|
||||
}
|
||||
|
||||
RAIIIsolateWithoutEntering::RAIIIsolateWithoutEntering(const SnapshotData* data)
|
||||
: allocator_{ArrayBuffer::Allocator::NewDefaultAllocator()} {
|
||||
isolate_ = Isolate::Allocate();
|
||||
|
||||
+12
@@ -641,6 +641,18 @@ class ArrayBufferViewContents {
|
||||
bool was_detached_ = false;
|
||||
};
|
||||
|
||||
// Creates a BackingStore with the contents of |data|. |deleter| runs once V8
|
||||
// no longer needs |data|: on release of the store when it can reference
|
||||
// |data| directly, or before this returns when V8 requires backing stores
|
||||
// inside its sandbox and |data| was copied. Returns nullptr if that copy
|
||||
// could not be allocated; |deleter| has run in that case too.
|
||||
std::unique_ptr<v8::BackingStore> AdoptIntoBackingStore(
|
||||
v8::Isolate* isolate,
|
||||
void* data,
|
||||
size_t byte_length,
|
||||
v8::BackingStore::DeleterCallback deleter,
|
||||
void* deleter_data);
|
||||
|
||||
class Utf8Value : public MaybeStackBuffer<char> {
|
||||
public:
|
||||
explicit Utf8Value(v8::Isolate* isolate, v8::Local<v8::Value> value);
|
||||
|
||||
@@ -10,12 +10,27 @@ using node::DataQueue;
|
||||
using v8::ArrayBuffer;
|
||||
using v8::BackingStore;
|
||||
|
||||
// Backing stores must lie inside V8's sandbox when it is enabled, so copy
|
||||
// the test data into memory from the default allocator rather than wrap it.
|
||||
static std::shared_ptr<BackingStore> MakeStore(const char* data) {
|
||||
static ArrayBuffer::Allocator* const allocator =
|
||||
ArrayBuffer::Allocator::NewDefaultAllocator();
|
||||
size_t len = strlen(data);
|
||||
void* copy = allocator->AllocateUninitialized(len);
|
||||
CHECK_NOT_NULL(copy);
|
||||
memcpy(copy, data, len);
|
||||
return ArrayBuffer::NewBackingStore(
|
||||
copy,
|
||||
len,
|
||||
[](void* p, size_t n, void*) { allocator->Free(p, n); },
|
||||
nullptr);
|
||||
}
|
||||
|
||||
TEST(DataQueue, InMemoryEntry) {
|
||||
char buffer[] = "hello world";
|
||||
size_t len = strlen(buffer);
|
||||
|
||||
std::shared_ptr<BackingStore> store = ArrayBuffer::NewBackingStore(
|
||||
&buffer, len, [](void*, size_t, void*) {}, nullptr);
|
||||
std::shared_ptr<BackingStore> store = MakeStore(buffer);
|
||||
|
||||
// We can create an InMemoryEntry from a v8::BackingStore.
|
||||
std::unique_ptr<DataQueue::Entry> entry =
|
||||
@@ -87,11 +102,9 @@ TEST(DataQueue, IdempotentDataQueue) {
|
||||
size_t len2 = strlen(buffer2);
|
||||
size_t len3 = strlen(buffer3);
|
||||
|
||||
std::shared_ptr<BackingStore> store1 = ArrayBuffer::NewBackingStore(
|
||||
&buffer1, len1, [](void*, size_t, void*) {}, nullptr);
|
||||
std::shared_ptr<BackingStore> store1 = MakeStore(buffer1);
|
||||
|
||||
std::shared_ptr<BackingStore> store2 = ArrayBuffer::NewBackingStore(
|
||||
&buffer2, len2, [](void*, size_t, void*) {}, nullptr);
|
||||
std::shared_ptr<BackingStore> store2 = MakeStore(buffer2);
|
||||
|
||||
std::vector<std::unique_ptr<DataQueue::Entry>> list;
|
||||
list.push_back(
|
||||
@@ -124,8 +137,7 @@ TEST(DataQueue, IdempotentDataQueue) {
|
||||
// The size is known to be the sum of the in memory-entries.
|
||||
CHECK_EQ(data_queue->size().value(), len1 + len2);
|
||||
|
||||
std::shared_ptr<BackingStore> store3 = ArrayBuffer::NewBackingStore(
|
||||
&buffer3, len3, [](void*, size_t, void*) {}, nullptr);
|
||||
std::shared_ptr<BackingStore> store3 = MakeStore(buffer3);
|
||||
|
||||
// Trying to append a new entry does not crash, but returns std::nullopt.
|
||||
CHECK(!data_queue
|
||||
@@ -416,14 +428,11 @@ TEST(DataQueue, NonIdempotentDataQueue) {
|
||||
size_t len2 = strlen(buffer2);
|
||||
size_t len3 = strlen(buffer3);
|
||||
|
||||
std::shared_ptr<BackingStore> store1 = ArrayBuffer::NewBackingStore(
|
||||
&buffer1, len1, [](void*, size_t, void*) {}, nullptr);
|
||||
std::shared_ptr<BackingStore> store1 = MakeStore(buffer1);
|
||||
|
||||
std::shared_ptr<BackingStore> store2 = ArrayBuffer::NewBackingStore(
|
||||
&buffer2, len2, [](void*, size_t, void*) {}, nullptr);
|
||||
std::shared_ptr<BackingStore> store2 = MakeStore(buffer2);
|
||||
|
||||
std::shared_ptr<BackingStore> store3 = ArrayBuffer::NewBackingStore(
|
||||
&buffer3, len3, [](void*, size_t, void*) {}, nullptr);
|
||||
std::shared_ptr<BackingStore> store3 = MakeStore(buffer3);
|
||||
|
||||
// We can create an non-idempotent DataQueue from a list of entries.
|
||||
std::shared_ptr<DataQueue> data_queue = DataQueue::Create();
|
||||
@@ -550,11 +559,9 @@ TEST(DataQueue, DataQueueEntry) {
|
||||
size_t len1 = strlen(buffer1);
|
||||
size_t len2 = strlen(buffer2);
|
||||
|
||||
std::shared_ptr<BackingStore> store1 = ArrayBuffer::NewBackingStore(
|
||||
&buffer1, len1, [](void*, size_t, void*) {}, nullptr);
|
||||
std::shared_ptr<BackingStore> store1 = MakeStore(buffer1);
|
||||
|
||||
std::shared_ptr<BackingStore> store2 = ArrayBuffer::NewBackingStore(
|
||||
&buffer2, len2, [](void*, size_t, void*) {}, nullptr);
|
||||
std::shared_ptr<BackingStore> store2 = MakeStore(buffer2);
|
||||
|
||||
std::vector<std::unique_ptr<DataQueue::Entry>> list;
|
||||
list.push_back(
|
||||
|
||||
@@ -554,7 +554,11 @@ TEST_F(EnvironmentTest, BufferWithFreeCallbackIsDetached) {
|
||||
}
|
||||
|
||||
CHECK_EQ(callback_calls, 1);
|
||||
#ifdef V8_ENABLE_SANDBOX
|
||||
CHECK_EQ(ab->ByteLength(), sizeof(hello));
|
||||
#else
|
||||
CHECK_EQ(ab->ByteLength(), 0);
|
||||
#endif
|
||||
}
|
||||
|
||||
#if HAVE_INSPECTOR
|
||||
|
||||
@@ -250,8 +250,8 @@ napi_value NapiLinkedWithInstanceData(napi_env env, napi_value exports) {
|
||||
napi_value key, value;
|
||||
CHECK_EQ(napi_create_string_utf8(env, "hello", NAPI_AUTO_LENGTH, &key),
|
||||
napi_ok);
|
||||
CHECK_EQ(napi_create_external_arraybuffer(
|
||||
env, instance_data, 1, nullptr, nullptr, &value),
|
||||
CHECK_EQ(napi_create_bigint_uint64(
|
||||
env, reinterpret_cast<uint64_t>(instance_data), &value),
|
||||
napi_ok);
|
||||
CHECK_EQ(napi_set_property(env, exports, key, value), napi_ok);
|
||||
return nullptr;
|
||||
@@ -290,9 +290,9 @@ TEST_F(LinkedBindingTest, LocallyDefinedLinkedBindingNapiInstanceDataTest) {
|
||||
.ToLocalChecked();
|
||||
v8::Local<v8::Value> completion_value =
|
||||
script->Run(context).ToLocalChecked();
|
||||
CHECK(completion_value->IsArrayBuffer());
|
||||
instance_data =
|
||||
static_cast<int*>(completion_value.As<v8::ArrayBuffer>()->Data());
|
||||
CHECK(completion_value->IsBigInt());
|
||||
instance_data = reinterpret_cast<int*>(
|
||||
completion_value.As<v8::BigInt>()->Uint64Value());
|
||||
CHECK_NE(instance_data, nullptr);
|
||||
CHECK_EQ(*instance_data, 0);
|
||||
}
|
||||
@@ -328,9 +328,9 @@ TEST_F(LinkedBindingTest,
|
||||
.ToLocalChecked();
|
||||
v8::Local<v8::Value> completion_value =
|
||||
script->Run(context).ToLocalChecked();
|
||||
CHECK(completion_value->IsArrayBuffer());
|
||||
instance_data =
|
||||
static_cast<int*>(completion_value.As<v8::ArrayBuffer>()->Data());
|
||||
CHECK(completion_value->IsBigInt());
|
||||
instance_data = reinterpret_cast<int*>(
|
||||
completion_value.As<v8::BigInt>()->Uint64Value());
|
||||
CHECK_NE(instance_data, nullptr);
|
||||
CHECK_EQ(*instance_data, 0);
|
||||
}
|
||||
|
||||
@@ -273,6 +273,13 @@ Indicates if [internationalization][] is supported.
|
||||
|
||||
Indicates whether `IPv6` is supported on this platform.
|
||||
|
||||
### `hasV8Sandbox`
|
||||
|
||||
* [\<boolean>][<boolean>]
|
||||
|
||||
Indicates whether V8 was built with its sandbox enabled, in which case
|
||||
`ArrayBuffer`s cannot reference memory outside of it.
|
||||
|
||||
### `hasSQLite`
|
||||
|
||||
* [\<boolean>][<boolean>]
|
||||
|
||||
@@ -36,6 +36,7 @@ const bits = ['arm64', 'loong64', 'mips', 'mipsel', 'ppc64', 'riscv64', 's390x',
|
||||
.includes(process.arch) ? 64 : 32;
|
||||
const hasIntl = !!process.config.variables.v8_enable_i18n_support;
|
||||
const hasTemporal = !!process.config.variables.v8_enable_temporal_support;
|
||||
const hasV8Sandbox = !!process.config.variables.v8_enable_sandbox;
|
||||
|
||||
// small-icu doesn't support non-English locales
|
||||
const hasFullICU = (() => {
|
||||
@@ -1019,6 +1020,7 @@ const common = {
|
||||
getTTYfd,
|
||||
hasIntl,
|
||||
hasTemporal,
|
||||
hasV8Sandbox,
|
||||
hasFullICU,
|
||||
hasCrypto,
|
||||
hasDtls,
|
||||
|
||||
@@ -24,6 +24,7 @@ const {
|
||||
hasLocalStorage,
|
||||
hasIntl,
|
||||
hasTemporal,
|
||||
hasV8Sandbox,
|
||||
hasIPv6,
|
||||
isAIX,
|
||||
isAlive,
|
||||
@@ -83,6 +84,7 @@ export {
|
||||
hasLocalStorage,
|
||||
hasIntl,
|
||||
hasTemporal,
|
||||
hasV8Sandbox,
|
||||
hasIPv6,
|
||||
isAIX,
|
||||
isAlive,
|
||||
|
||||
@@ -76,7 +76,9 @@ test('ffi supports unaligned memory access', () => {
|
||||
}));
|
||||
});
|
||||
|
||||
test('ffi toBuffer supports copy and zero-copy views', () => {
|
||||
const zeroCopy = { skip: common.hasV8Sandbox && 'zero-copy views are unavailable with the V8 sandbox' };
|
||||
|
||||
test('ffi toBuffer supports copy and zero-copy views', zeroCopy, () => {
|
||||
withAllocations(common.mustCall((alloc) => {
|
||||
const ptr = alloc(8);
|
||||
ffi.exportBuffer(Buffer.from([1, 2, 3, 4]), ptr, 4);
|
||||
@@ -98,7 +100,7 @@ test('ffi toBuffer supports copy and zero-copy views', () => {
|
||||
}));
|
||||
});
|
||||
|
||||
test('ffi toArrayBuffer supports copy and zero-copy views', () => {
|
||||
test('ffi toArrayBuffer supports copy and zero-copy views', zeroCopy, () => {
|
||||
withAllocations(common.mustCall((alloc) => {
|
||||
const ptr = alloc(4);
|
||||
ffi.exportBuffer(Buffer.from([10, 20, 30, 40]), ptr, 4);
|
||||
@@ -120,6 +122,16 @@ test('ffi toArrayBuffer supports copy and zero-copy views', () => {
|
||||
}));
|
||||
});
|
||||
|
||||
test('ffi zero-copy views throw with the V8 sandbox', { skip: !common.hasV8Sandbox }, () => {
|
||||
withAllocations(common.mustCall((alloc) => {
|
||||
const ptr = alloc(4);
|
||||
ffi.exportBuffer(Buffer.from([1, 2, 3, 4]), ptr, 4);
|
||||
assert.deepStrictEqual([...ffi.toBuffer(ptr, 4)], [1, 2, 3, 4]);
|
||||
assert.throws(() => ffi.toBuffer(ptr, 4, false), { code: 'ERR_OPERATION_FAILED' });
|
||||
assert.throws(() => ffi.toArrayBuffer(ptr, 4, false), { code: 'ERR_OPERATION_FAILED' });
|
||||
}));
|
||||
});
|
||||
|
||||
test('ffi getRawPointer returns raw addresses for byte sources', () => {
|
||||
const buffer = Buffer.from([1, 2, 3]);
|
||||
const arrayBuffer = new Uint8Array([4, 5, 6, 7]).buffer;
|
||||
|
||||
@@ -13,6 +13,10 @@ if (common.isASan) {
|
||||
common.skip('ASan does not play well with secure heap allocations');
|
||||
}
|
||||
|
||||
if (common.hasV8Sandbox) {
|
||||
common.skip('--secure-heap is not available with the V8 sandbox');
|
||||
}
|
||||
|
||||
const {
|
||||
isBoringSSL,
|
||||
hasOpenSSL,
|
||||
|
||||
@@ -15,31 +15,30 @@ common.skipIfPerfettoEnabled();
|
||||
const { createTracing, getEnabledCategories } = require('trace_events');
|
||||
const assert = require('assert');
|
||||
|
||||
const binding = require('internal/test/binding');
|
||||
const getCategoryEnabledBuffer = binding.internalBinding('trace_events').getCategoryEnabledBuffer;
|
||||
const { categoryEnabledChecker } = require('internal/trace_events');
|
||||
|
||||
it('should track enabled/disabled categories', () => {
|
||||
const random = Math.random().toString().slice(2);
|
||||
const category = `node.${random}`;
|
||||
|
||||
const buffer = getCategoryEnabledBuffer(category);
|
||||
const isEnabled = categoryEnabledChecker(category);
|
||||
|
||||
const tracing = createTracing({
|
||||
categories: [category],
|
||||
});
|
||||
|
||||
assert.ok(buffer[0] === 0, `the buffer[0] should start with value 0, got: ${buffer[0]}`);
|
||||
assert.strictEqual(isEnabled(), false);
|
||||
|
||||
tracing.enable();
|
||||
|
||||
let currentCategories = getEnabledCategories();
|
||||
|
||||
assert.ok(currentCategories.includes(category), `the getEnabledCategories should include ${category}, got: ${currentCategories}`);
|
||||
assert.ok(buffer[0] > 0, `the buffer[0] should be greater than 0, got: ${buffer[0]}`);
|
||||
assert.strictEqual(isEnabled(), true);
|
||||
|
||||
tracing.disable();
|
||||
|
||||
currentCategories = getEnabledCategories();
|
||||
assert.ok(currentCategories === undefined, `the getEnabledCategories should return undefined, got: ${currentCategories}`);
|
||||
assert.ok(buffer[0] === 0, `the buffer[0] should be 0, got: ${buffer[0]}`);
|
||||
assert.strictEqual(isEnabled(), false);
|
||||
});
|
||||
Reference in new issue
Block a user