GeezerVison is showing github.com — process: add execve · nodejs/node@dc91ae7
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 |
|---|
@@ -1318,6 +1318,14 @@ added: v16.18.0 | 1318 | 1318 |
| 1319 | 1319 | Emitted when a new process is created. | 1320 | 1320 |
| | 1321 | +`execve` | | 1322 | + | | 1323 | +* `execPath` {string} | | 1324 | +* `args` {string\[]} | | 1325 | +* `env` {string\[]} | | 1326 | + | | 1327 | +Emitted when [`process.execve()`][] is invoked. | | 1328 | + | 1321 | 1329 | #### Worker Thread | 1322 | 1330 |
| 1323 | 1331 | <!-- YAML | @@ -1347,5 +1355,6 @@ Emitted when a new thread is created. | 1347 | 1355 | [`end` event]: #endevent | 1348 | 1356 | [`error` event]: #errorevent | 1349 | 1357 | [`net.Server.listen()`]: net.md#serverlisten | | 1358 | +[`process.execve()`]: process.md#processexecvefile-args-env | 1350 | 1359 | [`start` event]: #startevent | 1351 | 1360 | [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 |
|---|
@@ -2491,8 +2491,7 @@ if (process.getuid) { | 2491 | 2491 | } | 2492 | 2492 | ``` | 2493 | 2493 |
| 2494 | | -This function is only available on POSIX platforms (i.e. not Windows or | 2495 | | -Android). | | 2494 | +This function not available on Windows. | 2496 | 2495 |
| 2497 | 2496 | ## `process.hasUncaughtExceptionCaptureCallback()` | 2498 | 2497 |
| @@ -3314,6 +3313,33 @@ In custom builds from non-release versions of the source tree, only the | 3314 | 3313 | `name` property may be present. The additional properties should not be | 3315 | 3314 | relied upon to exist. | 3316 | 3315 |
| | 3316 | +## `process.execve(file[, args[, env]])` | | 3317 | +
| | 3318 | +<!-- YAML | | 3319 | +added: REPLACEME | | 3320 | +--> | | 3321 | +
| | 3322 | +> Stability: 1 - Experimental | | 3323 | +
| | 3324 | +* `file` {string} The name or path of the executable file to run. | | 3325 | +* `args` {string\[]} List of string arguments. No argument can contain a null-byte (`\u0000`). | | 3326 | +* `env` {Object} Environment key-value pairs. | | 3327 | + No key or value can contain a null-byte (`\u0000`). | | 3328 | + **Default:** `process.env`. | | 3329 | +
| | 3330 | +Replaces the current process with a new process. | | 3331 | +
| | 3332 | +This is achieved by using the `execve` POSIX function and therefore no memory or other | | 3333 | +resources from the current process are preserved, except for the standard input, | | 3334 | +standard output and standard error file descriptor. | | 3335 | +
| | 3336 | +All other resources are discarded by the system when the processes are swapped, without triggering | | 3337 | +any exit or close events and without running any cleanup handler. | | 3338 | +
| | 3339 | +This function will never return, unless an error occurred. | | 3340 | +
| | 3341 | +This function is only available on POSIX platforms (i.e. not Windows or Android). | | 3342 | +
| 3317 | 3343 | ## `process.report` | 3318 | 3344 |
| 3319 | 3345 | <!-- 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 |
|---|
@@ -179,6 +179,7 @@ const rawMethods = internalBinding('process_methods'); | 179 | 179 | process.availableMemory = rawMethods.availableMemory; | 180 | 180 | process.kill = wrapped.kill; | 181 | 181 | process.exit = wrapped.exit; | | 182 | + process.execve = wrapped.execve; | 182 | 183 | process.ref = perThreadSetup.ref; | 183 | 184 | process.unref = perThreadSetup.unref; | 184 | 185 |
| |
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, | @@ -35,19 +37,26 @@ const { | 35 | 37 | ErrnoException, | 36 | 38 | codes: { | 37 | 39 | ERR_ASSERTION, | | 40 | + ERR_FEATURE_UNAVAILABLE_ON_PLATFORM, | 38 | 41 | ERR_INVALID_ARG_TYPE, | 39 | 42 | ERR_INVALID_ARG_VALUE, | 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. | @@ -105,6 +114,7 @@ function wrapProcessMethods(binding) { | 105 | 114 | rss, | 106 | 115 | resourceUsage: _resourceUsage, | 107 | 116 | loadEnvFile: _loadEnvFile, | | 117 | + execve: _execve, | 108 | 118 | } = binding; | 109 | 119 |
| 110 | 120 | function _rawDebug(...args) { | @@ -227,6 +237,55 @@ function wrapProcessMethods(binding) { | 227 | 237 | return true; | 228 | 238 | } | 229 | 239 |
| | 240 | + function execve(execPath, args, env) { | | 241 | + emitExperimentalWarning('process.execve'); | | 242 | + | | 243 | + const { isMainThread } = require('internal/worker'); | | 244 | + | | 245 | + if (!isMainThread) { | | 246 | + throw new ERR_WORKER_UNSUPPORTED_OPERATION('Calling process.execve'); | | 247 | + } else if (process.platform === 'win32') { | | 248 | + throw new ERR_FEATURE_UNAVAILABLE_ON_PLATFORM('process.execve'); | | 249 | + } | | 250 | + | | 251 | + validateString(execPath, 'execPath'); | | 252 | + validateArray(args, 'args'); | | 253 | + | | 254 | + for (let i = 0; i < args.length; i++) { | | 255 | + const arg = args[i]; | | 256 | + if (typeof arg !== 'string' || StringPrototypeIncludes(arg, '\u0000')) { | | 257 | + throw new ERR_INVALID_ARG_VALUE(`args[${i}]`, arg, 'must be a string without null bytes'); | | 258 | + } | | 259 | + } | | 260 | + | | 261 | + const envArray = []; | | 262 | + if (env !== undefined) { | | 263 | + validateObject(env, 'env'); | | 264 | + | | 265 | + for (const { 0: key, 1: value } of ObjectEntries(env)) { | | 266 | + if ( | | 267 | + typeof key !== 'string' || | | 268 | + typeof value !== 'string' || | | 269 | + StringPrototypeIncludes(key, '\u0000') || | | 270 | + StringPrototypeIncludes(value, '\u0000') | | 271 | + ) { | | 272 | + throw new ERR_INVALID_ARG_VALUE( | | 273 | + 'env', env, 'must be an object with string keys and values without null bytes', | | 274 | + ); | | 275 | + } else { | | 276 | + ArrayPrototypePush(envArray, `${key}=${value}`); | | 277 | + } | | 278 | + } | | 279 | + } | | 280 | + | | 281 | + if (execveDiagnosticChannel.hasSubscribers) { | | 282 | + execveDiagnosticChannel.publish({ execPath, args, env: envArray }); | | 283 | + } | | 284 | + | | 285 | + // Perform the system call | | 286 | + _execve(execPath, args, envArray); | | 287 | + } | | 288 | + | 230 | 289 | const resourceValues = new Float64Array(16); | 231 | 290 | function resourceUsage() { | 232 | 291 | _resourceUsage(resourceValues); | @@ -271,6 +330,7 @@ function wrapProcessMethods(binding) { | 271 | 330 | memoryUsage, | 272 | 331 | kill, | 273 | 332 | exit, | | 333 | + execve, | 274 | 334 | loadEnvFile, | 275 | 335 | }; | 276 | 336 | } | |
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 { | @@ -471,6 +473,95 @@ static void ReallyExit(const FunctionCallbackInfo<Value>& args) { | 471 | 473 | env->Exit(code); | 472 | 474 | } | 473 | 475 |
| | 476 | +#ifdef __POSIX__ | | 477 | +inline int persist_standard_stream(int fd) { | | 478 | + int flags = fcntl(fd, F_GETFD, 0); | | 479 | + | | 480 | + if (flags < 0) { | | 481 | + return flags; | | 482 | + } | | 483 | + | | 484 | + flags &= ~FD_CLOEXEC; | | 485 | + return fcntl(fd, F_SETFD, flags); | | 486 | +} | | 487 | + | | 488 | +static void Execve(const FunctionCallbackInfo<Value>& args) { | | 489 | + Environment* env = Environment::GetCurrent(args); | | 490 | + Isolate* isolate = env->isolate(); | | 491 | + Local<Context> context = env->context(); | | 492 | + | | 493 | + THROW_IF_INSUFFICIENT_PERMISSIONS( | | 494 | + env, permission::PermissionScope::kChildProcess, ""); | | 495 | + | | 496 | + CHECK(args[0]->IsString()); | | 497 | + CHECK(args[1]->IsArray()); | | 498 | + CHECK(args[2]->IsArray()); | | 499 | + | | 500 | + Local<Array> argv_array = args[1].As<Array>(); | | 501 | + Local<Array> envp_array = args[2].As<Array>(); | | 502 | + | | 503 | + // Copy arguments and environment | | 504 | + Utf8Value executable(isolate, args[0]); | | 505 | + std::vector<std::string> argv_strings(argv_array->Length()); | | 506 | + std::vector<std::string> envp_strings(envp_array->Length()); | | 507 | + std::vector<char*> argv(argv_array->Length() + 1); | | 508 | + std::vector<char*> envp(envp_array->Length() + 1); | | 509 | + | | 510 | + for (unsigned int i = 0; i < argv_array->Length(); i++) { | | 511 | + Local<Value> str; | | 512 | + if (!argv_array->Get(context, i).ToLocal(&str)) { | | 513 | + THROW_ERR_INVALID_ARG_VALUE(env, "Failed to deserialize argument."); | | 514 | + return; | | 515 | + } | | 516 | + | | 517 | + argv_strings[i] = Utf8Value(isolate, str).ToString(); | | 518 | + argv[i] = argv_strings[i].data(); | | 519 | + } | | 520 | + argv[argv_array->Length()] = nullptr; | | 521 | + | | 522 | + for (unsigned int i = 0; i < envp_array->Length(); i++) { | | 523 | + Local<Value> str; | | 524 | + if (!envp_array->Get(context, i).ToLocal(&str)) { | | 525 | + THROW_ERR_INVALID_ARG_VALUE( | | 526 | + env, "Failed to deserialize environment variable."); | | 527 | + return; | | 528 | + } | | 529 | + | | 530 | + envp_strings[i] = Utf8Value(isolate, str).ToString(); | | 531 | + envp[i] = envp_strings[i].data(); | | 532 | + } | | 533 | + | | 534 | + envp[envp_array->Length()] = nullptr; | | 535 | + | | 536 | + // Set stdin, stdout and stderr to be non-close-on-exec | | 537 | + // so that the new process will inherit it. | | 538 | + if (persist_standard_stream(0) < 0 || persist_standard_stream(1) < 0 || | | 539 | + persist_standard_stream(2) < 0) { | | 540 | + env->ThrowErrnoException(errno, "fcntl"); | | 541 | + return; | | 542 | + } | | 543 | + | | 544 | + // Perform the execve operation. | | 545 | + RunAtExit(env); | | 546 | + execve(*executable, argv.data(), envp.data()); | | 547 | + | | 548 | + // If it returns, it means that the execve operation failed. | | 549 | + // In that case we abort the process. | | 550 | + auto error_message = std::string("process.execve failed with error code ") + | | 551 | + errors::errno_string(errno); | | 552 | + | | 553 | + // Abort the process | | 554 | + Local<v8::Value> exception = | | 555 | + ErrnoException(isolate, errno, "execve", *executable); | | 556 | + Local<v8::Message> message = v8::Exception::CreateMessage(isolate, exception); | | 557 | + | | 558 | + std::string info = FormatErrorMessage( | | 559 | + isolate, context, error_message.c_str(), message, true); | | 560 | + FPrintF(stderr, "%s\n", info); | | 561 | + ABORT(); | | 562 | +} | | 563 | +#endif | | 564 | + | 474 | 565 | static void LoadEnvFile(const v8::FunctionCallbackInfo<v8::Value>& args) { | 475 | 566 | Environment* env = Environment::GetCurrent(args); | 476 | 567 | std::string path = ".env"; | @@ -662,6 +753,10 @@ static void CreatePerIsolateProperties(IsolateData* isolate_data, | 662 | 753 | SetMethodNoSideEffect(isolate, target, "cwd", Cwd); | 663 | 754 | SetMethod(isolate, target, "dlopen", binding::DLOpen); | 664 | 755 | SetMethod(isolate, target, "reallyExit", ReallyExit); | | 756 | + | | 757 | +#ifdef __POSIX__ | | 758 | + SetMethod(isolate, target, "execve", Execve); | | 759 | +#endif | 665 | 760 | SetMethodNoSideEffect(isolate, target, "uptime", Uptime); | 666 | 761 | SetMethod(isolate, target, "patchProcessObject", PatchProcessObject); | 667 | 762 |
| @@ -704,6 +799,10 @@ void RegisterExternalReferences(ExternalReferenceRegistry* registry) { | 704 | 799 | registry->Register(Cwd); | 705 | 800 | registry->Register(binding::DLOpen); | 706 | 801 | registry->Register(ReallyExit); | | 802 | + | | 803 | +#ifdef __POSIX__ | | 804 | + registry->Register(Execve); | | 805 | +#endif | 707 | 806 | registry->Register(Uptime); | 708 | 807 | registry->Register(PatchProcessObject); | 709 | 808 |
| |
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 | +} |
|