Skip to content

Commit 3afc022

Browse files
committed
doc,lib,src: throw on missing WebAssembly or RX support
Signed-off-by: Paolo Insogna <paolo@cowtech.it>
1 parent bbd566d commit 3afc022

18 files changed

Lines changed: 308 additions & 42 deletions

‎BUILDING.md‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,12 @@ and libc version. The table below lists the support tier for each supported
9898
combination. A list of [supported compile toolchains](#supported-toolchains) is
9999
also supplied for tier 1 platforms.
100100

101+
Some built-in functionality requires WebAssembly or runtime allocation of
102+
executable memory. When a capability is unavailable, Node.js does not guarantee
103+
an alternative implementation of the affected feature. See the
104+
[`--jitless` documentation](doc/api/cli.md#--jitless) and
105+
[FFI documentation](doc/api/ffi.md) for the corresponding limitations and errors.
106+
101107
**For production applications, run Node.js on supported platforms only (Tier 1 or 2).**
102108

103109
Node.js does not support a platform version if a vendor has expired support

‎doc/api/cli.md‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2209,6 +2209,16 @@ Disable [runtime allocation of executable memory][jitless]. This may be
22092209
required on some platforms for security reasons. It can also reduce attack
22102210
surface on other platforms, but the performance impact may be severe.
22112211

2212+
When WebAssembly is unavailable in this mode, TypeScript parsing and WebAssembly
2213+
module imports throw [`ERR_WEBASSEMBLY_NOT_SUPPORTED`][]. Node.js does not
2214+
guarantee alternative implementations of features that require WebAssembly or
2215+
runtime allocation of executable memory when those capabilities are unavailable.
2216+
2217+
This flag controls V8's runtime code generation. It does not impose an
2218+
operating-system restriction on executable memory allocated by native addons or
2219+
[`node:ffi`][]. FFI operations that require unavailable executable memory can
2220+
throw [`ERR_RX_MEMORY_NOT_SUPPORTED`][].
2221+
22122222
### `--localstorage-file=file`
22132223

22142224
<!-- YAML
@@ -4928,7 +4938,9 @@ node --stack-trace-limit=12 -p -e "Error.stackTraceLimit" # prints 12
49284938
[`Buffer`]: buffer.md#class-buffer
49294939
[`CRYPTO_secure_malloc_init`]: https://www.openssl.org/docs/man3.0/man3/CRYPTO_secure_malloc_init.html
49304940
[`ERR_INVALID_TYPESCRIPT_SYNTAX`]: errors.md#err_invalid_typescript_syntax
4941+
[`ERR_RX_MEMORY_NOT_SUPPORTED`]: errors.md#err_rx_memory_not_supported
49314942
[`ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX`]: errors.md#err_unsupported_typescript_syntax
4943+
[`ERR_WEBASSEMBLY_NOT_SUPPORTED`]: errors.md#err_webassembly_not_supported
49324944
[`NODE_OPTIONS`]: #node_optionsoptions
49334945
[`NODE_USE_ENV_PROXY=1`]: #node_use_env_proxy1
49344946
[`NODE_V8_COVERAGE=dir`]: #node_v8_coveragedir

‎doc/api/errors.md‎

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2964,6 +2964,22 @@ added:
29642964
An attempt was made to `require()` an [ES Module][] while another `import()` call
29652965
was already in progress to load it asynchronously.
29662966

2967+
<a id="ERR_RX_MEMORY_NOT_SUPPORTED"></a>
2968+
2969+
### `ERR_RX_MEMORY_NOT_SUPPORTED`
2970+
2971+
<!-- YAML
2972+
added: REPLACEME
2973+
-->
2974+
2975+
A feature requiring runtime allocation of executable memory was used in an
2976+
environment where that capability is unavailable. This includes creating an
2977+
otherwise eligible FFI Fast API function, or allocating an FFI callback when
2978+
libffi cannot provide a closure and executable memory is unavailable.
2979+
2980+
Node.js does not guarantee a non-generated-code fallback for these operations.
2981+
This error can be caught without terminating the process.
2982+
29672983
<a id="ERR_SCRIPT_EXECUTION_INTERRUPTED"></a>
29682984

29692985
### `ERR_SCRIPT_EXECUTION_INTERRUPTED`
@@ -3653,6 +3669,11 @@ A feature requiring WebAssembly was used, but WebAssembly is not supported or
36533669
has been disabled in the current environment (for example, when running with
36543670
`--jitless`).
36553671

3672+
TypeScript parsing and WebAssembly module imports, including source phase
3673+
imports, report this error when WebAssembly is unavailable. Node.js does not
3674+
provide an alternative implementation for these operations. This error can be
3675+
caught without terminating the process.
3676+
36563677
<a id="ERR_WEBASSEMBLY_RESPONSE"></a>
36573678

36583679
### `ERR_WEBASSEMBLY_RESPONSE`

‎doc/api/esm.md‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -745,6 +745,11 @@ imports is supported.
745745
Both of these integrations are in line with the
746746
[ES Module Integration Proposal for WebAssembly][].
747747
748+
When WebAssembly is unavailable in the current environment, importing a Wasm
749+
module throws [`ERR_WEBASSEMBLY_NOT_SUPPORTED`](errors.md#err_webassembly_not_supported).
750+
This also applies to source phase imports. Node.js does not provide an
751+
alternative implementation when WebAssembly is unavailable.
752+
748753
### Wasm Source Phase Imports
749754
750755
> Stability: 1.2 - Release candidate

‎doc/api/ffi.md‎

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,13 @@ The following targets are not supported by bundled libffi:
4545
When using the [Permission Model][], FFI APIs are
4646
restricted unless the [`--allow-ffi`][] flag is provided.
4747

48+
Some FFI operations require runtime allocation of executable memory. If an
49+
otherwise eligible Fast API function cannot be created because executable memory
50+
is unavailable, creation throws
51+
[`ERR_RX_MEMORY_NOT_SUPPORTED`](errors.md#err_rx_memory_not_supported) rather than
52+
selecting another call path. Callback allocation can also report this error.
53+
Loading the module or a library does not itself require generated trampolines.
54+
4855
## Overview
4956

5057
The `node:ffi` module exposes two groups of APIs:
@@ -465,6 +472,11 @@ Returns an object containing all previously resolved symbol addresses.
465472

466473
Creates a native callback pointer backed by a JavaScript function.
467474

475+
If libffi cannot allocate a closure and executable memory is unavailable, this
476+
method throws [`ERR_RX_MEMORY_NOT_SUPPORTED`](errors.md#err_rx_memory_not_supported).
477+
Other closure allocation failures throw `ERR_FFI_CALL_FAILED`. Platform-specific
478+
libffi implementations may provide callbacks without dynamically generated code.
479+
468480
When `signature` is omitted, the callback uses a default `void ()` signature.
469481

470482
The return value is the callback pointer address as a `bigint`. It can be
@@ -640,7 +652,8 @@ met:
640652
PPC64 always use another call path.
641653
* The process can allocate executable memory. Node.js checks once per process
642654
whether it can allocate memory and mark it executable. If that check fails,
643-
this path is disabled for the entire process.
655+
creating an otherwise eligible function throws
656+
[`ERR_RX_MEMORY_NOT_SUPPORTED`](errors.md#err_rx_memory_not_supported).
644657
* Neither the return type nor any argument type is `function`.
645658
* The signature has at most 8 arguments, and every argument fits in the
646659
argument registers available to the trampoline on the current platform.
@@ -650,8 +663,10 @@ The register limits are platform-specific. Integer and pointer-like arguments
650663
share one set of registers, and floating-point arguments share another. The
651664
limits for each architecture are listed in [Type names][].
652665

653-
A signature that fails any of these checks is not an error. The function is
654-
created on the next call path that supports it.
666+
A signature that is unsupported by the Fast API types, argument limits, or
667+
platform is not an error. The function is created on the next call path that
668+
supports it. Missing executable-memory support is checked only after signature
669+
and platform eligibility and does not select another call path.
655670

656671
### Shared buffer call path
657672

‎doc/api/typescript.md‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,11 @@ Node.js will replace TypeScript syntax with whitespace,
8282
and no type checking is performed.
8383
To disable this feature, use the flag [`--no-strip-types`][].
8484

85+
The built-in TypeScript parser requires WebAssembly. If WebAssembly is
86+
unavailable, parsing throws
87+
[`ERR_WEBASSEMBLY_NOT_SUPPORTED`](errors.md#err_webassembly_not_supported).
88+
Node.js does not provide a non-WebAssembly parser as a fallback.
89+
8590
Node.js ignores `tsconfig.json` files and therefore
8691
features that depend on settings within `tsconfig.json`,
8792
such as paths or converting newer JavaScript syntax to older standards, are

‎lib/internal/modules/esm/translators.js‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ const { emitExperimentalWarning, kEmptyObject, setOwnProperty, isWindows } = req
5454
const {
5555
ERR_INVALID_RETURN_PROPERTY_VALUE,
5656
ERR_UNKNOWN_BUILTIN_MODULE,
57+
ERR_WEBASSEMBLY_NOT_SUPPORTED,
5758
} = require('internal/errors').codes;
5859
const { maybeCacheSourceMap } = require('internal/source_map/source_map_cache');
5960
const moduleWrap = internalBinding('module_wrap');
@@ -558,6 +559,9 @@ translators.set('wasm', function(url, translateContext) {
558559
const { source } = translateContext;
559560
// WebAssembly global is not available during snapshot building, so we need to get it lazily.
560561
const { WebAssembly } = globalThis;
562+
if (WebAssembly === undefined) {
563+
throw new ERR_WEBASSEMBLY_NOT_SUPPORTED('WebAssembly module imports');
564+
}
561565
assertBufferSource(source, false, 'load');
562566

563567
debug(`Translating WASMModule ${url}`, translateContext);

‎src/ffi/fast.cc‎

Lines changed: 22 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -330,37 +330,29 @@ bool IsFastLibraryGuardSupported() {
330330
#endif
331331
}
332332

333-
std::unique_ptr<FastFFIMetadata> CreateFastFFIMetadata(const FFIFunction& fn,
334-
const bool* closed,
335-
v8::Isolate* isolate) {
336-
// Bail early if executable memory allocation doesn't work on this process
337-
// (missing MAP_JIT entitlement, hardened runtime, SELinux execmem, etc.).
338-
// The self-test runs once and caches the result.
339-
if (!IsJitMemorySupported()) {
340-
return nullptr;
341-
}
342-
333+
v8::Maybe<std::unique_ptr<FastFFIMetadata>> CreateFastFFIMetadata(
334+
const FFIFunction& fn, const bool* closed, v8::Isolate* isolate) {
343335
// Check signature-level eligibility (type checks, register caps, platform
344-
// support). Returning nullptr here lets the caller fall back to SharedBuffer
336+
// support). Null metadata lets the caller fall back to SharedBuffer
345337
// or the generic libffi path.
346338
const char* eligibility_reason;
347339
if (!IsFastCallEligible(fn, &eligibility_reason)) {
348-
return nullptr;
340+
return v8::Just(std::unique_ptr<FastFFIMetadata>());
349341
}
350342

351-
// Reject unsupported result types first. Returning nullptr means the caller
343+
// Reject unsupported result types first. Null metadata means the caller
352344
// can still fall back to SharedBuffer or the generic libffi path.
353345
FastFFIType result;
354346
if (!FastScalarTypeFromName(fn.return_type_name, &result)) {
355-
return nullptr;
347+
return v8::Just(std::unique_ptr<FastFFIMetadata>());
356348
}
357349
if (fn.args.size() != fn.arg_type_names.size()) {
358-
return nullptr;
350+
return v8::Just(std::unique_ptr<FastFFIMetadata>());
359351
}
360352
// Keep the initial Fast API implementation bounded to signatures V8 and the
361353
// platform trampolines can describe without stack argument support.
362354
if (fn.arg_type_names.size() > 8) {
363-
return nullptr;
355+
return v8::Just(std::unique_ptr<FastFFIMetadata>());
364356
}
365357

366358
std::vector<FastFFIType> args;
@@ -373,25 +365,35 @@ std::unique_ptr<FastFFIMetadata> CreateFastFFIMetadata(const FFIFunction& fn,
373365
for (const std::string& name : fn.arg_type_names) {
374366
FastFFIType type;
375367
if (!FastArgTypeFromName(name, &type)) {
376-
return nullptr;
368+
return v8::Just(std::unique_ptr<FastFFIMetadata>());
377369
}
378370
if (type == FastFFIType::kVoid) {
379-
return nullptr;
371+
return v8::Just(std::unique_ptr<FastFFIMetadata>());
380372
}
381373
needs_bigint = needs_bigint || NeedsBigIntRepresentation(type);
382374
needs_callback_options =
383375
needs_callback_options || type == FastFFIType::kBuffer;
384376
args.push_back(type);
385377
}
386378

379+
// Check RX memory only after signature and platform eligibility, so ordinary
380+
// unsupported signatures can still use the non-generated invocation paths.
381+
if (!IsJitMemorySupported()) {
382+
THROW_ERR_RX_MEMORY_NOT_SUPPORTED(
383+
isolate,
384+
"Executable memory is not supported in this environment, "
385+
"but is required for FFI Fast API calls");
386+
return v8::Nothing<std::unique_ptr<FastFFIMetadata>>();
387+
}
388+
387389
auto metadata = std::make_unique<FastFFIMetadata>();
388390
// The platform-specific trampoline is the executable entrypoint V8 calls.
389391
// If the platform rejects the signature, the whole fast metadata object is
390392
// discarded and the caller chooses another invocation path.
391393
FastFFITrampolineConfig config{fn.ptr, closed, isolate};
392394
if (!node_ffi_create_fast_trampoline(
393395
config, args.data(), args.size(), result, &metadata->trampoline)) {
394-
return nullptr;
396+
return v8::Just(std::unique_ptr<FastFFIMetadata>());
395397
}
396398

397399
metadata->arg_info.reserve(args.size() + 1);
@@ -418,7 +420,7 @@ std::unique_ptr<FastFFIMetadata> CreateFastFFIMetadata(const FFIFunction& fn,
418420
metadata->c_function =
419421
v8::CFunction(metadata->trampoline.code, metadata->c_function_info.get());
420422
metadata->guards_library = guards_library;
421-
return metadata;
423+
return v8::Just(std::move(metadata));
422424
}
423425

424426
} // namespace node::ffi

‎src/ffi/fast.h‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -73,9 +73,10 @@ std::shared_ptr<FFIFunction> CloneWithRawPointerArgNames(
7373
const std::shared_ptr<FFIFunction>& fn);
7474
std::shared_ptr<FFIFunction> CloneWithFastBufferArgNames(
7575
const std::shared_ptr<FFIFunction>& fn);
76-
std::unique_ptr<FastFFIMetadata> CreateFastFFIMetadata(const FFIFunction& fn,
77-
const bool* closed,
78-
v8::Isolate* isolate);
76+
// A null metadata value allows signature/platform fallback. Nothing means an
77+
// exception was thrown because an eligible signature requires RX memory.
78+
v8::Maybe<std::unique_ptr<FastFFIMetadata>> CreateFastFFIMetadata(
79+
const FFIFunction& fn, const bool* closed, v8::Isolate* isolate);
7980

8081
} // namespace node::ffi
8182

‎src/ffi/jit_memory.cc‎

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -23,13 +23,6 @@ namespace node::ffi {
2323
namespace {
2424

2525
bool SelfTest() {
26-
#if !defined(__aarch64__) && !defined(_M_ARM64) && !defined(__x86_64__) && \
27-
!defined(_M_X64) && !defined(__powerpc64__) && !defined(__ppc64__) && \
28-
!defined(__PPC64__) && !defined(__loongarch64) && \
29-
!(defined(__riscv) && __riscv_xlen == 64) && !defined(__s390x__)
30-
// No stub emitter for this platform; nothing to test.
31-
return false;
32-
#else
3326
#if defined(__aarch64__) || defined(_M_ARM64)
3427
// AArch64 BR LR: 0xD65F03C0
3528
constexpr uint32_t kInstruction = 0xD65F03C0;
@@ -51,7 +44,8 @@ bool SelfTest() {
5144
constexpr uint16_t kInstruction = 0x07fe;
5245
constexpr size_t kInstructionSize = sizeof(uint16_t);
5346
#else
54-
// x86_64 RET: 0xC3
47+
// The probe is never executed, so this byte also works on platforms without
48+
// a Fast API stub emitter. On x86_64 it represents RET.
5549
constexpr uint8_t kInstruction = 0xC3;
5650
constexpr size_t kInstructionSize = sizeof(uint8_t);
5751
#endif
@@ -97,7 +91,7 @@ bool SelfTest() {
9791
defined(__ppc64__) || defined(__PPC64__) || defined(__loongarch64) || \
9892
(defined(__riscv) && __riscv_xlen == 64) || defined(__s390x__)
9993
std::memcpy(code, &kInstruction, kInstructionSize);
100-
#elif defined(__x86_64__)
94+
#else
10195
code[0] = kInstruction;
10296
#endif
10397

@@ -129,7 +123,6 @@ bool SelfTest() {
129123
munmap(page, page_size);
130124
return ok;
131125
#endif
132-
#endif
133126
}
134127

135128
} // namespace

0 commit comments

Comments
 (0)