Skip to content

Commit 4652302

Browse files
committed
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>
1 parent 21f0f27 commit 4652302

23 files changed

Lines changed: 218 additions & 177 deletions

‎lib/ffi.js‎

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -256,11 +256,10 @@ function exportString(str, data, len, encoding = 'utf8') {
256256
throw new ERR_OUT_OF_RANGE('len', `>= ${requiredLength}`, len);
257257
}
258258

259-
const targetBuffer = toBuffer(data, len, false);
260-
const dataLength = sourceBuffer.length;
261-
262-
sourceBuffer.copy(targetBuffer, 0, 0, dataLength);
263-
targetBuffer.fill(0, dataLength, dataLength + terminatorSize);
259+
const terminated = Buffer.allocUnsafe(requiredLength);
260+
sourceBuffer.copy(terminated);
261+
terminated.fill(0, sourceBuffer.length);
262+
exportBytes(terminated, data, len);
264263
}
265264

266265
function exportBuffer(source, data, len) {

‎lib/internal/http.js‎

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ const {
1010

1111
const { setUnrefTimeout } = require('internal/timers');
1212
const {
13-
getCategoryEnabledBuffer,
13+
categoryEnabledChecker,
1414
trace,
1515
nodeTraceEventCategory,
1616
kAsyncBegin,
@@ -44,11 +44,7 @@ function getNextTraceEventId() {
4444
return ++traceEventId;
4545
}
4646

47-
const httpEnabled = getCategoryEnabledBuffer('node.http');
48-
49-
function isTraceHTTPEnabled() {
50-
return httpEnabled[0] > 0;
51-
}
47+
const isTraceHTTPEnabled = categoryEnabledChecker('node.http');
5248

5349
const traceEventCategory = nodeTraceEventCategory('node.http');
5450

‎lib/internal/trace_events.js‎

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
'use strict';
22

3-
const { getCategoryEnabledBuffer, trace, usePerfetto } = internalBinding('trace_events');
3+
const {
4+
getCategoryEnabledBuffer,
5+
isTraceCategoryEnabled,
6+
trace,
7+
usePerfetto,
8+
} = internalBinding('trace_events');
49
const {
510
CHAR_UPPERCASE_B,
611
CHAR_LOWERCASE_B,
@@ -17,6 +22,16 @@ if (usePerfetto) {
1722
nodeTraceEventCategory = (category) => `node,${category}`;
1823
}
1924

25+
// The returned function reads the category's enabled flag directly when the
26+
// binding can expose it to JS, and asks the tracing controller otherwise.
27+
function categoryEnabledChecker(category) {
28+
const buffer = getCategoryEnabledBuffer(category);
29+
if (buffer === undefined) {
30+
return () => isTraceCategoryEnabled(category);
31+
}
32+
return () => buffer[0] > 0;
33+
}
34+
2035
// The async events describe the execution of a single asynchronous operation, and are
2136
// used to measure the time spent in a single asynchronous operation.
2237
// Async events may overlap with each other. Different events do not have
@@ -44,7 +59,7 @@ const kTraceInstant = CHAR_LOWERCASE_N;
4459

4560
module.exports = {
4661
usePerfetto,
47-
getCategoryEnabledBuffer,
62+
categoryEnabledChecker,
4863
trace,
4964
nodeTraceEventCategory,
5065
kAsyncBegin,

‎lib/internal/util/debuglog.js‎

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ const {
1616
} = primordials;
1717
const { inspect, format, formatWithOptions } = require('internal/util/inspect');
1818
const {
19-
getCategoryEnabledBuffer,
19+
categoryEnabledChecker,
2020
trace,
2121
nodeTraceEventCategory,
2222
kAsyncBegin,
@@ -388,14 +388,14 @@ function debugWithTimer(set, cb) {
388388
}
389389

390390
const traceCategory = nodeTraceEventCategory(`node.${StringPrototypeToLowerCase(set)}`);
391-
let traceCategoryBuffer;
391+
let traceCategoryEnabled;
392392
let debugLogCategoryEnabled = false;
393393
let timerFlags = kNone;
394394

395395
function ensureTimerFlagsAreUpdated() {
396396
timerFlags &= ~kSkipTrace;
397397

398-
if (traceCategoryBuffer[0] === 0) {
398+
if (!traceCategoryEnabled()) {
399399
timerFlags |= kSkipTrace;
400400
}
401401
}
@@ -469,15 +469,15 @@ function debugWithTimer(set, cb) {
469469
}
470470
emitWarningIfNeeded(set);
471471
debugLogCategoryEnabled = testEnabled(set);
472-
traceCategoryBuffer = getCategoryEnabledBuffer(traceCategory);
472+
traceCategoryEnabled = categoryEnabledChecker(traceCategory);
473473

474474
timerFlags = kNone;
475475

476476
if (!debugLogCategoryEnabled) {
477477
timerFlags |= kSkipLog;
478478
}
479479

480-
if (traceCategoryBuffer[0] === 0) {
480+
if (!traceCategoryEnabled()) {
481481
timerFlags |= kSkipTrace;
482482
}
483483

‎src/crypto/crypto_dh.cc‎

Lines changed: 5 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,6 @@ using ncrypto::DHPointer;
2222
using ncrypto::EVPKeyCtxPointer;
2323
using ncrypto::EVPKeyPointer;
2424
using v8::ArrayBuffer;
25-
using v8::BackingStoreInitializationMode;
26-
using v8::BackingStoreOnFailureMode;
2725
using v8::ConstructorBehavior;
2826
using v8::Context;
2927
using v8::DontDelete;
@@ -60,21 +58,8 @@ MaybeLocal<Value> DataPointerToBuffer(Environment* env, DataPointer&& data) {
6058
struct Flag {
6159
bool secure;
6260
};
63-
#ifdef V8_ENABLE_SANDBOX
64-
auto backing = ArrayBuffer::NewBackingStore(
61+
auto backing = AdoptIntoBackingStore(
6562
env->isolate(),
66-
data.size(),
67-
BackingStoreInitializationMode::kUninitialized,
68-
BackingStoreOnFailureMode::kReturnNull);
69-
if (!backing) {
70-
THROW_ERR_MEMORY_ALLOCATION_FAILED(env);
71-
return MaybeLocal<Value>();
72-
}
73-
if (data.size() > 0) {
74-
memcpy(backing->Data(), data.get(), data.size());
75-
}
76-
#else
77-
auto backing = ArrayBuffer::NewBackingStore(
7863
data.get(),
7964
data.size(),
8065
[](void* data, size_t len, void* ptr) {
@@ -83,7 +68,10 @@ MaybeLocal<Value> DataPointerToBuffer(Environment* env, DataPointer&& data) {
8368
},
8469
new Flag{data.isSecure()});
8570
data.release();
86-
#endif // V8_ENABLE_SANDBOX
71+
if (!backing) {
72+
THROW_ERR_MEMORY_ALLOCATION_FAILED(env);
73+
return MaybeLocal<Value>();
74+
}
8775

8876
auto ab = ArrayBuffer::New(env->isolate(), std::move(backing));
8977
return Buffer::New(env, ab, 0, ab->ByteLength()).FromMaybe(Local<Value>());

‎src/crypto/crypto_util.cc‎

Lines changed: 13 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,6 @@ using v8::Array;
3232
using v8::ArrayBuffer;
3333
using v8::ArrayBufferView;
3434
using v8::BackingStore;
35-
using v8::BackingStoreInitializationMode;
36-
using v8::BackingStoreOnFailureMode;
3735
using v8::BigInt;
3836
using v8::Context;
3937
using v8::EscapableHandleScope;
@@ -446,33 +444,18 @@ std::unique_ptr<BackingStore> ByteSource::ReleaseToBackingStore(
446444
// It's ok for allocated_data_ to be nullptr but
447445
// only if size_ is zero.
448446
CHECK_IMPLIES(size_ > 0, allocated_data_ != nullptr);
449-
#ifdef V8_ENABLE_SANDBOX
450-
// If the v8 sandbox is enabled, then all array buffers must be allocated
451-
// via the isolate. External buffers are not allowed. So, instead of wrapping
452-
// the allocated data we'll copy it instead.
453-
454-
// TODO(@jasnell): It would be nice to use an abstracted utility to do this
455-
// branch instead of duplicating the V8_ENABLE_SANDBOX check each time.
456-
std::unique_ptr<BackingStore> ptr = ArrayBuffer::NewBackingStore(
447+
std::unique_ptr<BackingStore> ptr = AdoptIntoBackingStore(
457448
env->isolate(),
449+
allocated_data_,
458450
size(),
459-
BackingStoreInitializationMode::kUninitialized,
460-
BackingStoreOnFailureMode::kReturnNull);
451+
[](void* data, size_t length, void*) {
452+
OPENSSL_clear_free(data, length);
453+
},
454+
nullptr);
461455
if (!ptr) {
462456
THROW_ERR_MEMORY_ALLOCATION_FAILED(env);
463457
return nullptr;
464458
}
465-
memcpy(ptr->Data(), allocated_data_, size());
466-
OPENSSL_clear_free(allocated_data_, size_);
467-
#else
468-
std::unique_ptr<BackingStore> ptr = ArrayBuffer::NewBackingStore(
469-
allocated_data_,
470-
size(),
471-
[](void* data, size_t length, void* deleter_data) {
472-
OPENSSL_clear_free(deleter_data, length);
473-
}, allocated_data_);
474-
#endif // V8_ENABLE_SANDBOX
475-
CHECK(ptr);
476459
allocated_data_ = nullptr;
477460
data_ = nullptr;
478461
size_ = 0;
@@ -837,17 +820,6 @@ namespace {
837820
// initialized, SecureBuffer will automatically use it.
838821
void SecureBuffer(const FunctionCallbackInfo<Value>& args) {
839822
Environment* env = Environment::GetCurrent(args);
840-
#ifdef V8_ENABLE_SANDBOX
841-
// The v8 sandbox is enabled, so we cannot use the secure heap because
842-
// the sandbox requires that all array buffers be allocated via the isolate.
843-
// That is fundamentally incompatible with the secure heap which allocates
844-
// in openssl's secure heap area. Instead we'll just throw an error here.
845-
//
846-
// That said, we really shouldn't get here in the first place since the
847-
// option to enable the secure heap is only available when the sandbox
848-
// is disabled.
849-
UNREACHABLE();
850-
#else
851823
CHECK(args[0]->IsUint32());
852824
uint32_t len = args[0].As<Uint32>()->Value();
853825

@@ -857,7 +829,10 @@ void SecureBuffer(const FunctionCallbackInfo<Value>& args) {
857829
}
858830
auto released = data.release();
859831

860-
std::shared_ptr<BackingStore> store = ArrayBuffer::NewBackingStore(
832+
// Under V8_ENABLE_SANDBOX this ends up as a plain copy, which is fine:
833+
// --secure-heap is unavailable there, so SecureAlloc() is OPENSSL_malloc().
834+
std::shared_ptr<BackingStore> store = AdoptIntoBackingStore(
835+
env->isolate(),
861836
released.data,
862837
released.len,
863838
[](void* data, size_t len, void* deleter_data) {
@@ -871,10 +846,12 @@ void SecureBuffer(const FunctionCallbackInfo<Value>& args) {
871846
true);
872847
},
873848
nullptr);
849+
if (!store) {
850+
return THROW_ERR_MEMORY_ALLOCATION_FAILED(env);
851+
}
874852

875853
Local<ArrayBuffer> buffer = ArrayBuffer::New(env->isolate(), store);
876854
args.GetReturnValue().Set(Uint8Array::New(buffer, 0, len));
877-
#endif // V8_ENABLE_SANDBOX
878855
}
879856

880857
void SecureHeapUsed(const FunctionCallbackInfo<Value>& args) {

‎src/crypto/crypto_x509.cc‎

Lines changed: 5 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,6 @@ using ncrypto::X509View;
2727
using v8::Array;
2828
using v8::ArrayBuffer;
2929
using v8::ArrayBufferView;
30-
using v8::BackingStoreInitializationMode;
31-
using v8::BackingStoreOnFailureMode;
3230
using v8::Boolean;
3331
using v8::Context;
3432
using v8::Date;
@@ -140,29 +138,18 @@ MaybeLocal<Value> ToBuffer(Environment* env, BIOPointer* bio) {
140138
BUF_MEM* mem = *bio;
141139
if (!mem) [[unlikely]]
142140
return {};
143-
#ifdef V8_ENABLE_SANDBOX
144-
// If the v8 sandbox is enabled, then all array buffers must be allocated
145-
// via the isolate. External buffers are not allowed. So, instead of wrapping
146-
// the BIOPointer we'll copy it instead.
147-
auto backing = ArrayBuffer::NewBackingStore(
141+
auto backing = AdoptIntoBackingStore(
148142
env->isolate(),
149-
mem->length,
150-
BackingStoreInitializationMode::kUninitialized,
151-
BackingStoreOnFailureMode::kReturnNull);
152-
if (!backing) {
153-
THROW_ERR_MEMORY_ALLOCATION_FAILED(env);
154-
return MaybeLocal<Value>();
155-
}
156-
memcpy(backing->Data(), mem->data, mem->length);
157-
#else
158-
auto backing = ArrayBuffer::NewBackingStore(
159143
mem->data,
160144
mem->length,
161145
[](void*, size_t, void* data) {
162146
BIOPointer free_me(static_cast<BIO*>(data));
163147
},
164148
bio->release());
165-
#endif // V8_ENABLE_SANDBOX
149+
if (!backing) {
150+
THROW_ERR_MEMORY_ALLOCATION_FAILED(env);
151+
return MaybeLocal<Value>();
152+
}
166153
auto ab = ArrayBuffer::New(env->isolate(), std::move(backing));
167154
Local<Value> ret;
168155
if (!Buffer::New(env, ab, 0, ab->ByteLength()).ToLocal(&ret)) return {};

‎src/ffi/data.cc‎

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -535,6 +535,17 @@ void ToString(const FunctionCallbackInfo<Value>& args) {
535535
args.GetReturnValue().Set(out);
536536
}
537537

538+
// Foreign memory lies outside the V8 sandbox and cannot back an ArrayBuffer.
539+
static bool ZeroCopyUnavailable(Environment* env) {
540+
#ifdef V8_ENABLE_SANDBOX
541+
THROW_ERR_OPERATION_FAILED(
542+
env, "Zero-copy views are not available when the V8 sandbox is enabled");
543+
return true;
544+
#else
545+
return false;
546+
#endif
547+
}
548+
538549
void ToBuffer(const FunctionCallbackInfo<Value>& args) {
539550
Environment* env = Environment::GetCurrent(args);
540551
Isolate* isolate = env->isolate();
@@ -580,9 +591,12 @@ void ToBuffer(const FunctionCallbackInfo<Value>& args) {
580591
return;
581592
}
582593

594+
bool copy = args.Length() < 3 || args[2]->IsUndefined() ||
595+
args[2]->BooleanValue(isolate);
596+
if (!copy && ZeroCopyUnavailable(env)) return;
597+
583598
Local<Object> buf;
584-
if (args.Length() < 3 || args[2]->IsUndefined() ||
585-
args[2]->BooleanValue(isolate)) {
599+
if (copy) {
586600
if (!Buffer::Copy(env, reinterpret_cast<char*>(ptr), len).ToLocal(&buf)) {
587601
return;
588602
}
@@ -642,10 +656,12 @@ void ToArrayBuffer(const FunctionCallbackInfo<Value>& args) {
642656
return;
643657
}
644658

645-
Local<ArrayBuffer> ab;
659+
bool copy = args.Length() < 3 || args[2]->IsUndefined() ||
660+
args[2]->BooleanValue(isolate);
661+
if (!copy && ZeroCopyUnavailable(env)) return;
646662

647-
if (args.Length() < 3 || args[2]->IsUndefined() ||
648-
args[2]->BooleanValue(isolate)) {
663+
Local<ArrayBuffer> ab;
664+
if (copy) {
649665
std::unique_ptr<BackingStore> store =
650666
ArrayBuffer::NewBackingStore(isolate, len);
651667
memcpy(store->Data(), reinterpret_cast<void*>(ptr), len);

0 commit comments

Comments
 (0)