GeezerVison is showing github.com — process: add execve · nodejs/node@f8aff90
File treeExpand file treeCollapse file tree Open diff view settings Expand file treeCollapse file tree Open diff view settings Collapse file Copy file name to clipboardExpand all lines: doc/api/diagnostics_channel.md +9Lines changed: 9 additions & 0 deletions - Display the source diff
- Display the rich diff
| Original file line number | Diff line number | Diff line change |
|---|
@@ -1319,6 +1319,14 @@ added: v16.18.0 | 1319 | 1319 |
| 1320 | 1320 | Emitted when a new process is created. | 1321 | 1321 |
| | 1322 | +`execve` | | 1323 | + | | 1324 | +* `execPath` {string} | | 1325 | +* `args` {string\[]} | | 1326 | +* `env` {string\[]} | | 1327 | + | | 1328 | +Emitted when [`process.execve()`][] is invoked. | | 1329 | + | 1322 | 1330 | #### Worker Thread | 1323 | 1331 |
| 1324 | 1332 | <!-- YAML | @@ -1348,5 +1356,6 @@ Emitted when a new thread is created. | 1348 | 1356 | [`end` event]: #endevent | 1349 | 1357 | [`error` event]: #errorevent | 1350 | 1358 | [`net.Server.listen()`]: net.md#serverlisten | | 1359 | +[`process.execve()`]: process.md#processexecvefile-args-env | 1351 | 1360 | [`start` event]: #startevent | 1352 | 1361 | [context loss]: async_context.md#troubleshooting-context-loss |
Collapse file Copy file name to clipboardExpand all lines: doc/api/process.md +28-2Lines changed: 28 additions & 2 deletions - Display the source diff
- Display the rich diff
| Original file line number | Diff line number | Diff line change |
|---|
@@ -2502,8 +2502,7 @@ if (process.getuid) { | 2502 | 2502 | } | 2503 | 2503 | ``` | 2504 | 2504 |
| 2505 | | -This function is only available on POSIX platforms (i.e. not Windows or | 2506 | | -Android). | | 2505 | +This function not available on Windows. | 2507 | 2506 |
| 2508 | 2507 | ## `process.hasUncaughtExceptionCaptureCallback()` | 2509 | 2508 |
| @@ -3327,6 +3326,33 @@ In custom builds from non-release versions of the source tree, only the | 3327 | 3326 | `name` property may be present. The additional properties should not be | 3328 | 3327 | relied upon to exist. | 3329 | 3328 |
| | 3329 | +## `process.execve(file[, args[, env]])` | | 3330 | +
| | 3331 | +<!-- YAML | | 3332 | +added: REPLACEME | | 3333 | +--> | | 3334 | +
| | 3335 | +> Stability: 1 - Experimental | | 3336 | +
| | 3337 | +* `file` {string} The name or path of the executable file to run. | | 3338 | +* `args` {string\[]} List of string arguments. No argument can contain a null-byte (`\u0000`). | | 3339 | +* `env` {Object} Environment key-value pairs. | | 3340 | + No key or value can contain a null-byte (`\u0000`). | | 3341 | + **Default:** `process.env`. | | 3342 | +
| | 3343 | +Replaces the current process with a new process. | | 3344 | +
| | 3345 | +This is achieved by using the `execve` POSIX function and therefore no memory or other | | 3346 | +resources from the current process are preserved, except for the standard input, | | 3347 | +standard output and standard error file descriptor. | | 3348 | +
| | 3349 | +All other resources are discarded by the system when the processes are swapped, without triggering | | 3350 | +any exit or close events and without running any cleanup handler. | | 3351 | +
| | 3352 | +This function will never return, unless an error occurred. | | 3353 | +
| | 3354 | +This function is only available on POSIX platforms (i.e. not Windows or Android). | | 3355 | +
| 3330 | 3356 | ## `process.report` | 3331 | 3357 |
| 3332 | 3358 | <!-- YAML | |
Collapse file Copy file name to clipboardExpand all lines: lib/internal/bootstrap/node.js +1Lines changed: 1 addition & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -177,6 +177,7 @@ const rawMethods = internalBinding('process_methods'); | 177 | 177 | process.availableMemory = rawMethods.availableMemory; | 178 | 178 | process.kill = wrapped.kill; | 179 | 179 | process.exit = wrapped.exit; | | 180 | + process.execve = wrapped.execve; | 180 | 181 | process.ref = perThreadSetup.ref; | 181 | 182 | process.unref = perThreadSetup.unref; | 182 | 183 |
| |
Collapse file Copy file name to clipboardExpand all lines: lib/internal/process/per_thread.js +60Lines changed: 60 additions & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -16,6 +16,7 @@ const { | 16 | 16 | FunctionPrototypeCall, | 17 | 17 | NumberMAX_SAFE_INTEGER, | 18 | 18 | ObjectDefineProperty, | | 19 | + ObjectEntries, | 19 | 20 | ObjectFreeze, | 20 | 21 | ReflectApply, | 21 | 22 | RegExpPrototypeExec, | @@ -24,6 +25,7 @@ const { | 24 | 25 | SetPrototypeEntries, | 25 | 26 | SetPrototypeValues, | 26 | 27 | StringPrototypeEndsWith, | | 28 | + StringPrototypeIncludes, | 27 | 29 | StringPrototypeReplace, | 28 | 30 | StringPrototypeSlice, | 29 | 31 | Symbol, | @@ -34,20 +36,27 @@ const { | 34 | 36 | const { | 35 | 37 | ErrnoException, | 36 | 38 | codes: { | | 39 | + ERR_FEATURE_UNAVAILABLE_ON_PLATFORM, | 37 | 40 | ERR_INVALID_ARG_TYPE, | 38 | 41 | ERR_INVALID_ARG_VALUE, | 39 | 42 | ERR_OPERATION_FAILED, | 40 | 43 | ERR_OUT_OF_RANGE, | 41 | 44 | ERR_UNKNOWN_SIGNAL, | | 45 | + ERR_WORKER_UNSUPPORTED_OPERATION, | 42 | 46 | }, | 43 | 47 | } = require('internal/errors'); | | 48 | +const { emitExperimentalWarning } = require('internal/util'); | 44 | 49 | const format = require('internal/util/inspect').format; | 45 | 50 | const { | 46 | 51 | validateArray, | 47 | 52 | validateNumber, | 48 | 53 | validateObject, | | 54 | + validateString, | 49 | 55 | } = require('internal/validators'); | 50 | 56 |
| | 57 | +const dc = require('diagnostics_channel'); | | 58 | +const execveDiagnosticChannel = dc.channel('process.execve'); | | 59 | + | 51 | 60 | const constants = internalBinding('constants').os.signals; | 52 | 61 |
| 53 | 62 | let getValidatedPath; // We need to lazy load it because of the circular dependency. | @@ -103,6 +112,7 @@ function wrapProcessMethods(binding) { | 103 | 112 | rss, | 104 | 113 | resourceUsage: _resourceUsage, | 105 | 114 | loadEnvFile: _loadEnvFile, | | 115 | + execve: _execve, | 106 | 116 | } = binding; | 107 | 117 |
| 108 | 118 | function _rawDebug(...args) { | @@ -269,6 +279,55 @@ function wrapProcessMethods(binding) { | 269 | 279 | return true; | 270 | 280 | } | 271 | 281 |
| | 282 | + function execve(execPath, args, env) { | | 283 | + emitExperimentalWarning('process.execve'); | | 284 | + | | 285 | + const { isMainThread } = require('internal/worker'); | | 286 | + | | 287 | + if (!isMainThread) { | | 288 | + throw new ERR_WORKER_UNSUPPORTED_OPERATION('Calling process.execve'); | | 289 | + } else if (process.platform === 'win32') { | | 290 | + throw new ERR_FEATURE_UNAVAILABLE_ON_PLATFORM('process.execve'); | | 291 | + } | | 292 | + | | 293 | + validateString(execPath, 'execPath'); | | 294 | + validateArray(args, 'args'); | | 295 | + | | 296 | + for (let i = 0; i < args.length; i++) { | | 297 | + const arg = args[i]; | | 298 | + if (typeof arg !== 'string' || StringPrototypeIncludes(arg, '\u0000')) { | | 299 | + throw new ERR_INVALID_ARG_VALUE(`args[${i}]`, arg, 'must be a string without null bytes'); | | 300 | + } | | 301 | + } | | 302 | + | | 303 | + const envArray = []; | | 304 | + if (env !== undefined) { | | 305 | + validateObject(env, 'env'); | | 306 | + | | 307 | + for (const { 0: key, 1: value } of ObjectEntries(env)) { | | 308 | + if ( | | 309 | + typeof key !== 'string' || | | 310 | + typeof value !== 'string' || | | 311 | + StringPrototypeIncludes(key, '\u0000') || | | 312 | + StringPrototypeIncludes(value, '\u0000') | | 313 | + ) { | | 314 | + throw new ERR_INVALID_ARG_VALUE( | | 315 | + 'env', env, 'must be an object with string keys and values without null bytes', | | 316 | + ); | | 317 | + } else { | | 318 | + ArrayPrototypePush(envArray, `${key}=${value}`); | | 319 | + } | | 320 | + } | | 321 | + } | | 322 | + | | 323 | + if (execveDiagnosticChannel.hasSubscribers) { | | 324 | + execveDiagnosticChannel.publish({ execPath, args, env: envArray }); | | 325 | + } | | 326 | + | | 327 | + // Perform the system call | | 328 | + _execve(execPath, args, envArray); | | 329 | + } | | 330 | + | 272 | 331 | const resourceValues = new Float64Array(16); | 273 | 332 | function resourceUsage() { | 274 | 333 | _resourceUsage(resourceValues); | @@ -314,6 +373,7 @@ function wrapProcessMethods(binding) { | 314 | 373 | memoryUsage, | 315 | 374 | kill, | 316 | 375 | exit, | | 376 | + execve, | 317 | 377 | loadEnvFile, | 318 | 378 | }; | 319 | 379 | } | |
Collapse file Copy file name to clipboardExpand all lines: src/node_errors.h +5Lines changed: 5 additions & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
| 13 | 13 | #include <sstream> | 14 | 14 |
| 15 | 15 | namespace node { | | 16 | +// This forward declaration is required to have the method | | 17 | +// available in error messages. | | 18 | +namespace errors { | | 19 | +const char* errno_string(int errorno); | | 20 | +} | 16 | 21 |
| 17 | 22 | enum ErrorHandlingMode { CONTEXTIFY_ERROR, FATAL_ERROR, MODULE_ERROR }; | 18 | 23 | void AppendExceptionLine(Environment* env, | |
Collapse file Copy file name to clipboardExpand all lines: src/node_process_methods.cc +99Lines changed: 99 additions & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
| 27 | 27 | #if defined(_MSC_VER) | 28 | 28 | #include <direct.h> | 29 | 29 | #include <io.h> | | 30 | +#include <process.h> | 30 | 31 | #define umask _umask | 31 | 32 | typedef int mode_t; | 32 | 33 | #else | 33 | 34 | #include <pthread.h> | 34 | 35 | #include <sys/resource.h> // getrlimit, setrlimit | 35 | 36 | #include <termios.h> // tcgetattr, tcsetattr | | 37 | +#include <unistd.h> | 36 | 38 | #endif | 37 | 39 |
| 38 | 40 | namespace node { | @@ -495,6 +497,95 @@ static void ReallyExit(const FunctionCallbackInfo<Value>& args) { | 495 | 497 | env->Exit(code); | 496 | 498 | } | 497 | 499 |
| | 500 | +#ifdef __POSIX__ | | 501 | +inline int persist_standard_stream(int fd) { | | 502 | + int flags = fcntl(fd, F_GETFD, 0); | | 503 | + | | 504 | + if (flags < 0) { | | 505 | + return flags; | | 506 | + } | | 507 | + | | 508 | + flags &= ~FD_CLOEXEC; | | 509 | + return fcntl(fd, F_SETFD, flags); | | 510 | +} | | 511 | + | | 512 | +static void Execve(const FunctionCallbackInfo<Value>& args) { | | 513 | + Environment* env = Environment::GetCurrent(args); | | 514 | + Isolate* isolate = env->isolate(); | | 515 | + Local<Context> context = env->context(); | | 516 | + | | 517 | + THROW_IF_INSUFFICIENT_PERMISSIONS( | | 518 | + env, permission::PermissionScope::kChildProcess, ""); | | 519 | + | | 520 | + CHECK(args[0]->IsString()); | | 521 | + CHECK(args[1]->IsArray()); | | 522 | + CHECK(args[2]->IsArray()); | | 523 | + | | 524 | + Local<Array> argv_array = args[1].As<Array>(); | | 525 | + Local<Array> envp_array = args[2].As<Array>(); | | 526 | + | | 527 | + // Copy arguments and environment | | 528 | + Utf8Value executable(isolate, args[0]); | | 529 | + std::vector<std::string> argv_strings(argv_array->Length()); | | 530 | + std::vector<std::string> envp_strings(envp_array->Length()); | | 531 | + std::vector<char*> argv(argv_array->Length() + 1); | | 532 | + std::vector<char*> envp(envp_array->Length() + 1); | | 533 | + | | 534 | + for (unsigned int i = 0; i < argv_array->Length(); i++) { | | 535 | + Local<Value> str; | | 536 | + if (!argv_array->Get(context, i).ToLocal(&str)) { | | 537 | + THROW_ERR_INVALID_ARG_VALUE(env, "Failed to deserialize argument."); | | 538 | + return; | | 539 | + } | | 540 | + | | 541 | + argv_strings[i] = Utf8Value(isolate, str).ToString(); | | 542 | + argv[i] = argv_strings[i].data(); | | 543 | + } | | 544 | + argv[argv_array->Length()] = nullptr; | | 545 | + | | 546 | + for (unsigned int i = 0; i < envp_array->Length(); i++) { | | 547 | + Local<Value> str; | | 548 | + if (!envp_array->Get(context, i).ToLocal(&str)) { | | 549 | + THROW_ERR_INVALID_ARG_VALUE( | | 550 | + env, "Failed to deserialize environment variable."); | | 551 | + return; | | 552 | + } | | 553 | + | | 554 | + envp_strings[i] = Utf8Value(isolate, str).ToString(); | | 555 | + envp[i] = envp_strings[i].data(); | | 556 | + } | | 557 | + | | 558 | + envp[envp_array->Length()] = nullptr; | | 559 | + | | 560 | + // Set stdin, stdout and stderr to be non-close-on-exec | | 561 | + // so that the new process will inherit it. | | 562 | + if (persist_standard_stream(0) < 0 || persist_standard_stream(1) < 0 || | | 563 | + persist_standard_stream(2) < 0) { | | 564 | + env->ThrowErrnoException(errno, "fcntl"); | | 565 | + return; | | 566 | + } | | 567 | + | | 568 | + // Perform the execve operation. | | 569 | + RunAtExit(env); | | 570 | + execve(*executable, argv.data(), envp.data()); | | 571 | + | | 572 | + // If it returns, it means that the execve operation failed. | | 573 | + // In that case we abort the process. | | 574 | + auto error_message = std::string("process.execve failed with error code ") + | | 575 | + errors::errno_string(errno); | | 576 | + | | 577 | + // Abort the process | | 578 | + Local<v8::Value> exception = | | 579 | + ErrnoException(isolate, errno, "execve", *executable); | | 580 | + Local<v8::Message> message = v8::Exception::CreateMessage(isolate, exception); | | 581 | + | | 582 | + std::string info = FormatErrorMessage( | | 583 | + isolate, context, error_message.c_str(), message, true); | | 584 | + FPrintF(stderr, "%s\n", info); | | 585 | + ABORT(); | | 586 | +} | | 587 | +#endif | | 588 | + | 498 | 589 | static void LoadEnvFile(const v8::FunctionCallbackInfo<v8::Value>& args) { | 499 | 590 | Environment* env = Environment::GetCurrent(args); | 500 | 591 | std::string path = ".env"; | @@ -687,6 +778,10 @@ static void CreatePerIsolateProperties(IsolateData* isolate_data, | 687 | 778 | SetMethodNoSideEffect(isolate, target, "cwd", Cwd); | 688 | 779 | SetMethod(isolate, target, "dlopen", binding::DLOpen); | 689 | 780 | SetMethod(isolate, target, "reallyExit", ReallyExit); | | 781 | + | | 782 | +#ifdef __POSIX__ | | 783 | + SetMethod(isolate, target, "execve", Execve); | | 784 | +#endif | 690 | 785 | SetMethodNoSideEffect(isolate, target, "uptime", Uptime); | 691 | 786 | SetMethod(isolate, target, "patchProcessObject", PatchProcessObject); | 692 | 787 |
| @@ -730,6 +825,10 @@ void RegisterExternalReferences(ExternalReferenceRegistry* registry) { | 730 | 825 | registry->Register(Cwd); | 731 | 826 | registry->Register(binding::DLOpen); | 732 | 827 | registry->Register(ReallyExit); | | 828 | + | | 829 | +#ifdef __POSIX__ | | 830 | + registry->Register(Execve); | | 831 | +#endif | 733 | 832 | registry->Register(Uptime); | 734 | 833 | registry->Register(PatchProcessObject); | 735 | 834 |
| |
Collapse file Copy file name to clipboard +26Lines changed: 26 additions & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
| | 1 | +'use strict'; | | 2 | + | | 3 | +const { skip, isWindows } = require('../common'); | | 4 | +const { ok } = require('assert'); | | 5 | +const { spawnSync } = require('child_process'); | | 6 | +const { isMainThread } = require('worker_threads'); | | 7 | + | | 8 | +if (!isMainThread) { | | 9 | + skip('process.execve is not available in Workers'); | | 10 | +} else if (isWindows) { | | 11 | + skip('process.execve is not available in Windows'); | | 12 | +} | | 13 | + | | 14 | +if (process.argv[2] === 'child') { | | 15 | + process.execve( | | 16 | + process.execPath + '_non_existing', | | 17 | + [__filename, 'replaced'], | | 18 | + { ...process.env, EXECVE_A: 'FIRST', EXECVE_B: 'SECOND', CWD: process.cwd() } | | 19 | + ); | | 20 | +} else { | | 21 | + const child = spawnSync(`${process.execPath}`, [`${__filename}`, 'child']); | | 22 | + const stderr = child.stderr.toString(); | | 23 | + | | 24 | + ok(stderr.includes('process.execve failed with error code ENOENT'), stderr); | | 25 | + ok(stderr.includes('execve (node:internal/process/per_thread'), stderr); | | 26 | +} |
Collapse file Copy file name to clipboard +18Lines changed: 18 additions & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
| | 1 | +'use strict'; | | 2 | + | | 3 | +const { mustNotCall, skip, isWindows } = require('../common'); | | 4 | +const { strictEqual } = require('assert'); | | 5 | +const { isMainThread } = require('worker_threads'); | | 6 | + | | 7 | +if (!isMainThread) { | | 8 | + skip('process.execve is not available in Workers'); | | 9 | +} else if (isWindows) { | | 10 | + skip('process.execve is not available in Windows'); | | 11 | +} | | 12 | + | | 13 | +if (process.argv[2] === 'replaced') { | | 14 | + strictEqual(process.argv[2], 'replaced'); | | 15 | +} else { | | 16 | + process.on('exit', mustNotCall()); | | 17 | + process.execve(process.execPath, [process.execPath, __filename, 'replaced'], process.env); | | 18 | +} |
|