Commit 88ec31414c9 for nodejs

commit 88ec31414c9e308b91b19028a8090689d927c8be
Author: Joyee Cheung <joyeec9h3@gmail.com>
Date:   Mon Oct 5 13:25:07 2026 +0200

    Revert "deps,lib,tools: remove jitless and lite modes"

    Newer version of V8 no longer supports
    --allow-overwriting-for-next-flag and there's no way to
    override the preset flag now. Revert the commit to allow
    users to opt-in into the flags if still necessary.

    This reverts commit 7e39b8781aa25d4c3477f5add9d31a00b50e2802.

    Signed-off-by: Joyee Cheung <joyeec9h3@gmail.com>
    PR-URL: https://github.com/nodejs/node/pull/66516
    Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>
    Reviewed-By: Trivikram Kamat <trivikr.dev@gmail.com>
    Reviewed-By: Rafael Gonzaga <rafael.nunu@hotmail.com>

diff --git a/BUILDING.md b/BUILDING.md
index 795019c77db..6fc93ca2ad0 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 eec2250da6b..e769236cebe 100755
--- a/configure.py
+++ b/configure.py
@@ -1224,6 +1224,14 @@ parser.add_argument('--v8-with-dchecks',
     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 @@ parser.add_argument('--use-ccache-win',

 (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 d51eef7e2ac..4e17d81169d 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`
+
+<!-- YAML
+added: v12.0.0
+-->
+
+> 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`

 <!-- YAML
@@ -4398,6 +4411,7 @@ V8 options that are allowed are:
 * `--enable-etw-stack-walking`
 * `--expose-gc`
 * `--interpreted-frames-native-stack`
+* `--jitless`
 * `--max-heap-size`
 * `--max-old-space-size`
 * `--max-semi-space-size`
@@ -4768,6 +4782,8 @@ documented here:

 ### `--interpreted-frames-native-stack`

+### `--jitless`
+
 ### `--max-heap-size`

 Specifies the maximum heap size (in megabytes) for the process.
@@ -4960,6 +4976,7 @@ node --stack-trace-limit=12 -p -e "Error.stackTraceLimit" # prints 12
 [environment_variables]: #environment-variables-1
 [filtering tests by name]: test.md#filtering-tests-by-name
 [global setup and teardown]: test.md#global-setup-and-teardown
+[jitless]: https://v8.dev/blog/jitless
 [libuv threadpool documentation]: https://docs.libuv.org/en/latest/threadpool.html
 [module compile cache]: module.md#module-compile-cache
 [preloading asynchronous module customization hooks]: module.md#registration-of-asynchronous-customization-hooks
diff --git a/doc/api/errors.md b/doc/api/errors.md
index 2838eb5e65b..11292b60b5d 100644
--- a/doc/api/errors.md
+++ b/doc/api/errors.md
@@ -3645,6 +3645,14 @@ The WASI instance has already started.

 The WASI instance has not been started.

+<a id="ERR_WEBASSEMBLY_NOT_SUPPORTED"></a>
+
+### `ERR_WEBASSEMBLY_NOT_SUPPORTED`
+
+A feature requiring WebAssembly was used, but WebAssembly is not supported or
+has been disabled in the current environment (for example, when running with
+`--jitless`).
+
 <a id="ERR_WEBASSEMBLY_RESPONSE"></a>

 ### `ERR_WEBASSEMBLY_RESPONSE`
@@ -4480,18 +4488,6 @@ The linker function returned a module for which linking has failed.

 The module must be successfully linked before instantiation.

-<a id="ERR_WEBASSEMBLY_NOT_SUPPORTED"></a>
-
-### `ERR_WEBASSEMBLY_NOT_SUPPORTED`
-
-<!-- YAML
-removed: REPLACEME
--->
-
-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.
-
 <a id="ERR_WORKER_UNSUPPORTED_EXTENSION"></a>

 ### `ERR_WORKER_UNSUPPORTED_EXTENSION`
diff --git a/doc/node.1 b/doc/node.1
index 5db586dddd1..70488dff26c 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 59b13b6a07d..9240b00ac70 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 e7ef5aeba96..0e80f682817 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 9f1dbae7c9a..5dd425cb093 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 54600a50b6d..2f72e636ab9 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 ff4cb4b6ac5..d0b5310626c 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 911bb999ed2..c46b3baa775 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 4dc948113fb..14b74f4b6d4 100644
--- a/src/node_v8.cc
+++ b/src/node_v8.cc
@@ -272,10 +272,6 @@ void SetFlagsFromString(const FunctionCallbackInfo<Value>& 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<Value>& args) {
diff --git a/test/doctool/test-doc-api-json.mjs b/test/doctool/test-doc-api-json.mjs
index 3496652a819..ff063e018d0 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 00000000000..39f10491fa2
--- /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 9efe10ad61b..fd58d1d990a 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 96555e7c88f..00000000000
--- 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 f6832ab0c79..375c46c11ab 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 e96d2c7832b..b3a2503d1d5 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 1c50880f49f..8e81f42a945 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 04ad6339194..10c8f4c9d78 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',],
       }],