diff --git a/BUILDING.md b/BUILDING.md index 795019c77db9..6fc93ca2ad0e 100644 --- a/BUILDING.md +++ b/BUILDING.md @@ -98,9 +98,6 @@ and libc version. The table below lists the support tier for each supported combination. A list of [supported compile toolchains](#supported-toolchains) is also supplied for tier 1 platforms. -Node.js requires WebAssembly for built-in functionality, including `fetch()` and -TypeScript support. JIT-less execution and V8 lite mode are not supported. - **For production applications, run Node.js on supported platforms only (Tier 1 or 2).** Node.js does not support a platform version if a vendor has expired support diff --git a/configure.py b/configure.py index eec2250da6b1..e769236cebe8 100755 --- a/configure.py +++ b/configure.py @@ -1224,6 +1224,14 @@ default=False, help='compile V8 with debug checks and runtime debugging features enabled') +parser.add_argument('--v8-lite-mode', + action='store_true', + dest='v8_lite_mode', + default=False, + help='compile V8 in lite mode for constrained environments (lowers V8 '+ + 'memory footprint, but also implies no just-in-time compilation ' + + 'support, thus much slower execution)') + parser.add_argument('--v8-enable-object-print', action='store_true', dest='v8_enable_object_print', @@ -1310,10 +1318,6 @@ (options, args) = parser.parse_known_args() -# Reject the removed option before forwarding unknown arguments to GYP. -if any(arg.split('=', 1)[0] == '--v8-lite-mode' for arg in args): - parser.error('--v8-lite-mode is no longer supported; Node.js requires WebAssembly') - # Expand ~ in the install prefix now, it gets written to multiple files. options.prefix = str(Path(options.prefix or '').expanduser()) @@ -2227,8 +2231,9 @@ def configure_library(lib, output, pkgname=None): def configure_v8(o, configs): set_configuration_variable(configs, 'v8_enable_v8_checks', release=0, debug=1) - o['variables']['v8_enable_webassembly'] = 1 + o['variables']['v8_enable_webassembly'] = 0 if options.v8_lite_mode else 1 o['variables']['v8_enable_javascript_promise_hooks'] = 1 + o['variables']['v8_enable_lite_mode'] = 1 if options.v8_lite_mode else 0 is_gdbjit_supported_arch = ( 'x64' in o['variables']['target_arch'] or 'ia32' in o['variables']['target_arch'] or diff --git a/doc/api/cli.md b/doc/api/cli.md index d51eef7e2ac6..4e17d81169db 100644 --- a/doc/api/cli.md +++ b/doc/api/cli.md @@ -2196,6 +2196,19 @@ added: v0.7.7 Opens the REPL even if stdin does not appear to be a terminal. +### `--jitless` + + + +> Stability: 1 - Experimental. This flag is inherited from V8 and is subject to +> change upstream. + +Disable [runtime allocation of executable memory][jitless]. This may be +required on some platforms for security reasons. It can also reduce attack +surface on other platforms, but the performance impact may be severe. + ### `--localstorage-file=file` - -A feature requiring WebAssembly was used in an environment where WebAssembly -was unavailable. Node.js now requires WebAssembly and no longer supports -JIT-less execution. - ### `ERR_WORKER_UNSUPPORTED_EXTENSION` diff --git a/doc/node.1 b/doc/node.1 index 5db586dddd1b..70488dff26ca 100644 --- a/doc/node.1 +++ b/doc/node.1 @@ -1175,6 +1175,11 @@ See V8 Inspector integration for Node.js for further explanation on Node.js debu .It Fl i , Fl -interactive Opens the REPL even if stdin does not appear to be a terminal. . +.It Fl -jitless +Disable runtime allocation of executable memory. This may be +required on some platforms for security reasons. It can also reduce attack +surface on other platforms, but the performance impact may be severe. +. .It Fl -localstorage-file Ns = Ns Ar file The file used to store \fBlocalStorage\fR data. If the file does not exist, it is created the first time \fBlocalStorage\fR is accessed. The same file may be shared @@ -2523,6 +2528,8 @@ V8 options that are allowed are: .It \fB--interpreted-frames-native-stack\fR .It +\fB--jitless\fR +.It \fB--max-heap-size\fR .It \fB--max-old-space-size\fR diff --git a/lib/eslint.config_partial.mjs b/lib/eslint.config_partial.mjs index 59b13b6a07da..9240b00ac709 100644 --- a/lib/eslint.config_partial.mjs +++ b/lib/eslint.config_partial.mjs @@ -298,8 +298,8 @@ export default [ name: 'URLSearchParams', message: "Use `const { URLSearchParams } = require('internal/url');` instead of the global.", }, - // WebAssembly is unavailable during snapshot building and must be - // accessed lazily at runtime. + // WebAssembly is not available in primordials because it can be + // disabled with --jitless CLI flag. { name: 'WebAssembly', message: 'Use `const { WebAssembly } = globalThis;` instead of the global.', diff --git a/lib/internal/errors.js b/lib/internal/errors.js index e7ef5aeba96f..0e80f682817d 100644 --- a/lib/internal/errors.js +++ b/lib/internal/errors.js @@ -1981,6 +1981,9 @@ E('ERR_VM_MODULE_NOT_MODULE', 'Provided module is not an instance of Module', Error); E('ERR_VM_MODULE_STATUS', 'Module status %s', Error); E('ERR_WASI_ALREADY_STARTED', 'WASI instance has already started', Error); +E('ERR_WEBASSEMBLY_NOT_SUPPORTED', + 'WebAssembly is not supported in this environment, but is required for %s', + Error); E('ERR_WEBASSEMBLY_RESPONSE', 'WebAssembly response %s', TypeError); E('ERR_WORKER_HANDLE_NOT_TRANSFERABLE', '%s cannot be transferred in its current state; it must be a freshly ' + diff --git a/lib/internal/freeze_intrinsics.js b/lib/internal/freeze_intrinsics.js index 9f1dbae7c9a5..5dd425cb0934 100644 --- a/lib/internal/freeze_intrinsics.js +++ b/lib/internal/freeze_intrinsics.js @@ -363,16 +363,18 @@ module.exports = function() { ArrayPrototypePush(intrinsics, SharedArrayBuffer); } - ArrayPrototypePush(intrinsicPrototypes, - WebAssembly.Module.prototype, - WebAssembly.Instance.prototype, - WebAssembly.Table.prototype, - WebAssembly.Memory.prototype, - WebAssembly.CompileError.prototype, - WebAssembly.LinkError.prototype, - WebAssembly.RuntimeError.prototype, - ); - ArrayPrototypePush(intrinsics, WebAssembly); + if (typeof WebAssembly !== 'undefined') { + ArrayPrototypePush(intrinsicPrototypes, + WebAssembly.Module.prototype, + WebAssembly.Instance.prototype, + WebAssembly.Table.prototype, + WebAssembly.Memory.prototype, + WebAssembly.CompileError.prototype, + WebAssembly.LinkError.prototype, + WebAssembly.RuntimeError.prototype, + ); + ArrayPrototypePush(intrinsics, WebAssembly); + } if (typeof Intl !== 'undefined') { ArrayPrototypePush(intrinsicPrototypes, diff --git a/lib/internal/util.js b/lib/internal/util.js index 54600a50b6d7..2f72e636ab90 100644 --- a/lib/internal/util.js +++ b/lib/internal/util.js @@ -45,6 +45,7 @@ const { SymbolPrototypeGetDescription, SymbolReplace, SymbolSplit, + globalThis, } = primordials; const { @@ -52,6 +53,7 @@ const { ERR_NO_CRYPTO, ERR_NO_TYPESCRIPT, ERR_UNKNOWN_SIGNAL, + ERR_WEBASSEMBLY_NOT_SUPPORTED, }, isErrorStackTraceLimitWritable, overrideStackTrace, @@ -243,6 +245,8 @@ function assertCrypto() { function assertTypeScript() { if (noTypeScript) throw new ERR_NO_TYPESCRIPT(); + if (globalThis.WebAssembly === undefined) + throw new ERR_WEBASSEMBLY_NOT_SUPPORTED('TypeScript'); } /** diff --git a/src/node.cc b/src/node.cc index ff4cb4b6ac58..d0b5310626c3 100644 --- a/src/node.cc +++ b/src/node.cc @@ -48,10 +48,6 @@ #include "node_version.h" #include "permission/env_permission.h" -#if defined(V8_JITLESS) || defined(V8_LITE_MODE) -#error Node.js does not support interpreter-only V8 builds. -#endif - #if HAVE_OPENSSL #include "ncrypto.h" #if OPENSSL_VERSION_MAJOR >= 3 @@ -1112,12 +1108,6 @@ static ExitCode InitializeNodeWithArgsInternal( allow_env_sources.Finish(); - // Node.js requires WebAssembly for built-in functionality. Override these - // modes after all option sources, before V8 applies their implications. - // Explicitly allow overriding even when contradiction checks are enabled. - V8::SetFlagsFromString("--allow-overwriting-for-next-flag --no-lite-mode " - "--allow-overwriting-for-next-flag --no-jitless"); - // Every option source has now been parsed, so cross-source option // constraints can finally be validated. CheckGlobalBenchOptions(errors); diff --git a/src/node_options.cc b/src/node_options.cc index 911bb999ed2e..c46b3baa7753 100644 --- a/src/node_options.cc +++ b/src/node_options.cc @@ -1563,6 +1563,10 @@ PerIsolateOptionsParser::PerIsolateOptionsParser( "disallow eval and friends", V8Option{}, kAllowedInEnvvar); + AddOption("--jitless", + "disable runtime allocation of executable memory", + V8Option{}, + kAllowedInEnvvar); AddOption("--report-uncaught-exception", "generate diagnostic report on uncaught exceptions", BOOL_FIELD(report_uncaught_exception), diff --git a/src/node_v8.cc b/src/node_v8.cc index 4dc948113fbc..14b74f4b6d42 100644 --- a/src/node_v8.cc +++ b/src/node_v8.cc @@ -272,10 +272,6 @@ void SetFlagsFromString(const FunctionCallbackInfo& args) { CHECK(args[0]->IsString()); Utf8Value flags(args.GetIsolate(), args[0]); V8::SetFlagsFromString(flags.out(), flags.length()); - // Runtime flags must not re-enable modes that disable required WebAssembly - // support, even when contradiction checks are enabled. - V8::SetFlagsFromString("--allow-overwriting-for-next-flag --no-lite-mode " - "--allow-overwriting-for-next-flag --no-jitless"); } void StartCpuProfile(const FunctionCallbackInfo& args) { diff --git a/test/doctool/test-doc-api-json.mjs b/test/doctool/test-doc-api-json.mjs index 3496652a819b..ff063e018d0e 100644 --- a/test/doctool/test-doc-api-json.mjs +++ b/test/doctool/test-doc-api-json.mjs @@ -159,4 +159,4 @@ for await (const dirent of await fs.opendir(new URL('../../out/doc/api/', import } assert.strictEqual(numberOfDeprecatedSections, 49); // Increase this number every time a new API is deprecated. -assert.strictEqual(numberOfRemovedAPIs, 47); // Increase this number every time a section is marked as removed. +assert.strictEqual(numberOfRemovedAPIs, 46); // Increase this number every time a section is marked as removed. diff --git a/test/es-module/test-import-cjs-jitless.mjs b/test/es-module/test-import-cjs-jitless.mjs new file mode 100644 index 000000000000..39f10491fa28 --- /dev/null +++ b/test/es-module/test-import-cjs-jitless.mjs @@ -0,0 +1,6 @@ +// Flags: --jitless + +// Tests that importing a CJS module works in JIT-less mode (i.e. falling back to the +// JS parser if WASM is not available). +import '../common/index.mjs'; +import '../fixtures/empty.cjs'; diff --git a/test/es-module/test-typescript.mjs b/test/es-module/test-typescript.mjs index 9efe10ad61bf..fd58d1d990a5 100644 --- a/test/es-module/test-typescript.mjs +++ b/test/es-module/test-typescript.mjs @@ -318,3 +318,14 @@ test('execute invalid TypeScript syntax', async () => { assert.strictEqual(result.stdout, ''); assert.strictEqual(result.code, 1); }); + +test('expect error when executing a TypeScript file with --jitless', async () => { + const result = await spawnPromisified(process.execPath, [ + '--jitless', + fixtures.path('typescript/ts/test-typescript.ts'), + ]); + + assert.match(result.stderr, /ERR_WEBASSEMBLY_NOT_SUPPORTED/); + assert.match(result.stderr, /WebAssembly is not supported in this environment, but is required for TypeScript/); + assert.strictEqual(result.code, 1); +}); diff --git a/test/parallel/test-cli-no-jitless.js b/test/parallel/test-cli-no-jitless.js deleted file mode 100644 index 96555e7c88f9..000000000000 --- a/test/parallel/test-cli-no-jitless.js +++ /dev/null @@ -1,90 +0,0 @@ -'use strict'; - -const common = require('../common'); -const assert = require('assert'); -const { spawnSync } = require('child_process'); - -// Interpreter-only modes are overridden to preserve Node's WebAssembly -// requirement, including alternate spellings and the runtime flag API. -const wasm = ` - const assert = require('assert'); - assert.strictEqual(typeof WebAssembly, 'object'); - const bytes = Uint8Array.of(0, 97, 115, 109, 1, 0, 0, 0); - new WebAssembly.Instance(new WebAssembly.Module(bytes)); -`; - -for (const flag of [ - '--jitless', '--lite-mode', '--lite_mode', '-jitless', '-lite_mode', - '--no-jitless', '--no-lite-mode', -]) { - const result = spawnSync(process.execPath, [flag, '-e', wasm], { encoding: 'utf8' }); - assert.strictEqual(result.status, 0, result.stderr); - assert.strictEqual(result.signal, null); - assert.strictEqual(result.stderr, ''); - - const runtime = spawnSync(process.execPath, ['-e', ` - require('v8').setFlagsFromString(${JSON.stringify(flag)}); - ${wasm} - // A new isolate must also retain WebAssembly after changing runtime flags. - new (require('worker_threads').Worker)(${JSON.stringify(wasm)}, { eval: true }); - `], { encoding: 'utf8' }); - assert.strictEqual(runtime.status, 0, runtime.stderr); - assert.strictEqual(runtime.stderr, ''); -} - -// V8 boolean flags reject explicit values, even for overridden modes. -for (const flag of ['--jitless=true', '--lite_mode=true']) { - const result = spawnSync(process.execPath, [flag, '-e', wasm], { encoding: 'utf8' }); - assert.strictEqual(result.status, 9, result.stderr); - assert.strictEqual(result.signal, null); - assert.match(result.stderr, /illegal value for flag .* of type bool/); - - // The runtime API reports invalid syntax without throwing, and Node must - // still override any flags V8 changed before reporting the error. - const runtime = spawnSync(process.execPath, ['-e', ` - require('v8').setFlagsFromString(${JSON.stringify(flag)}); - ${wasm} - new (require('worker_threads').Worker)(${JSON.stringify(wasm)}, { eval: true }); - `], { encoding: 'utf8' }); - assert.strictEqual(runtime.status, 0, runtime.stderr); - assert.strictEqual(runtime.signal, null); - assert.match(runtime.stderr, /illegal value for flag .* of type bool/); -} - -// Node's overrides must not be treated as contradictory user-supplied flags. -for (const flag of ['--jitless', '--lite-mode']) { - const result = spawnSync(process.execPath, [ - '--abort-on-contradictory-flags', flag, '-e', wasm, - ], { encoding: 'utf8' }); - assert.strictEqual(result.status, 0, result.stderr); - assert.strictEqual(result.signal, null); - assert.strictEqual(result.stderr, ''); -} - -const options = spawnSync(process.execPath, ['--v8-options'], { encoding: 'utf8' }); -assert.strictEqual(options.status, 0, options.stderr); -assert.match(options.stdout, /--jitless\b/); -assert.match(options.stdout, /--lite-mode\b/); - -for (const flag of ['--jitless', '--lite-mode', '--lite_mode']) { - assert.strictEqual(process.allowedNodeEnvironmentFlags.has(flag), false); - if (!process.config.variables.node_without_node_options) { - const result = spawnSync(process.execPath, ['-e', ''], { - encoding: 'utf8', - env: { ...process.env, NODE_OPTIONS: flag }, - }); - assert.strictEqual(result.status, 9, result.stderr); - assert.match(result.stderr, /is not allowed in NODE_OPTIONS/); - } -} - -// Workers must inherit a runtime with working WebAssembly as well. -const { Worker } = require('worker_threads'); -const worker = new Worker(` - const { parentPort } = require('worker_threads'); - const bytes = Uint8Array.of(0, 97, 115, 109, 1, 0, 0, 0); - new WebAssembly.Instance(new WebAssembly.Module(bytes)); - parentPort.postMessage(typeof WebAssembly); -`, { eval: true }); -worker.on('message', common.mustCall((value) => assert.strictEqual(value, 'object'))); -worker.on('exit', common.mustCall((code) => assert.strictEqual(code, 0))); diff --git a/test/parallel/test-cli-node-options.js b/test/parallel/test-cli-node-options.js index f6832ab0c79b..375c46c11ab7 100644 --- a/test/parallel/test-cli-node-options.js +++ b/test/parallel/test-cli-node-options.js @@ -76,6 +76,7 @@ if (common.hasCrypto) { expect('--abort_on-uncaught_exception', 'B\n'); expect('--disallow-code-generation-from-strings', 'B\n'); expect('--expose-gc', 'B\n'); +expect('--jitless', 'B\n'); expect('--max-old-space-size=0', 'B\n'); expect('--max-semi-space-size=0', 'B\n'); expect('--stack-trace-limit=100', diff --git a/test/parallel/test-freeze-intrinsics.js b/test/parallel/test-freeze-intrinsics.js index e96d2c7832be..b3a2503d1d5b 100644 --- a/test/parallel/test-freeze-intrinsics.js +++ b/test/parallel/test-freeze-intrinsics.js @@ -1,11 +1,8 @@ -// Flags: --frozen-intrinsics +// Flags: --frozen-intrinsics --jitless 'use strict'; require('../common'); const assert = require('assert'); -assert(Object.isFrozen(WebAssembly)); -assert(Object.isFrozen(WebAssembly.Module.prototype)); - assert.throws( () => Object.defineProperty = 'asdf', TypeError diff --git a/test/parallel/test-internal-webidl-buffer-source.js b/test/parallel/test-internal-webidl-buffer-source.js index 1c50880f49fa..8e81f42a9456 100644 --- a/test/parallel/test-internal-webidl-buffer-source.js +++ b/test/parallel/test-internal-webidl-buffer-source.js @@ -326,7 +326,9 @@ test('Shared buffer growability checks do not read JavaScript properties', () => } }); -test('Shared WebAssembly buffer growability is checked per buffer', () => { +test('Shared WebAssembly buffer growability is checked per buffer', { + skip: typeof WebAssembly === 'undefined', +}, () => { const memory = new WebAssembly.Memory({ initial: 1, maximum: 2, shared: true }); for (const [buffer, growable] of [ [memory.buffer, false], diff --git a/tools/v8_gypfiles/features.gypi b/tools/v8_gypfiles/features.gypi index 04ad6339194f..10c8f4c9d788 100644 --- a/tools/v8_gypfiles/features.gypi +++ b/tools/v8_gypfiles/features.gypi @@ -309,6 +309,11 @@ # This option can be on unconditionally. 'v8_enable_temporal_systemicu%': 1, + # Lite mode disables a number of performance optimizations to reduce memory + # at the cost of performance. + # Sets --DV8_LITE_MODE. + 'v8_enable_lite_mode%': 0, + # Enable the Turbofan compiler. # Sets -dV8_ENABLE_TURBOFAN 'v8_enable_turbofan%': 1, @@ -346,6 +351,9 @@ ['v8_enable_future==1', { 'defines': ['V8_ENABLE_FUTURE',], }], + ['v8_enable_lite_mode==1', { + 'defines': ['V8_LITE_MODE',], + }], ['v8_enable_gdbjit==1', { 'defines': ['ENABLE_GDB_JIT_INTERFACE',], }],