Skip to content

Commit dc91ae7

Browse files
ShogunPandaRafaelGSS
authored andcommitted
process: add execve
PR-URL: #56496 Reviewed-By: Matteo Collina <matteo.collina@gmail.com> Reviewed-By: Rafael Gonzaga <rafael.nunu@hotmail.com> Reviewed-By: Bryan English <bryan@bryanenglish.com> Reviewed-By: James M Snell <jasnell@gmail.com>
1 parent 2cfe31c commit dc91ae7

14 files changed

+470
-2
lines changed

doc/api/diagnostics_channel.md

+9
Original file line numberDiff line numberDiff line change
@@ -1318,6 +1318,14 @@ added: v16.18.0
13181318

13191319
Emitted when a new process is created.
13201320

1321+
`execve`
1322+
1323+
* `execPath` {string}
1324+
* `args` {string\[]}
1325+
* `env` {string\[]}
1326+
1327+
Emitted when [`process.execve()`][] is invoked.
1328+
13211329
#### Worker Thread
13221330

13231331
<!-- YAML
@@ -1347,5 +1355,6 @@ Emitted when a new thread is created.
13471355
[`end` event]: #endevent
13481356
[`error` event]: #errorevent
13491357
[`net.Server.listen()`]: net.md#serverlisten
1358+
[`process.execve()`]: process.md#processexecvefile-args-env
13501359
[`start` event]: #startevent
13511360
[context loss]: async_context.md#troubleshooting-context-loss

doc/api/process.md

+28-2
Original file line numberDiff line numberDiff line change
@@ -2491,8 +2491,7 @@ if (process.getuid) {
24912491
}
24922492
```
24932493
2494-
This function is only available on POSIX platforms (i.e. not Windows or
2495-
Android).
2494+
This function not available on Windows.
24962495
24972496
## `process.hasUncaughtExceptionCaptureCallback()`
24982497
@@ -3314,6 +3313,33 @@ In custom builds from non-release versions of the source tree, only the
33143313
`name` property may be present. The additional properties should not be
33153314
relied upon to exist.
33163315
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+
33173343
## `process.report`
33183344
33193345
<!-- YAML

lib/internal/bootstrap/node.js

+1
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,7 @@ const rawMethods = internalBinding('process_methods');
179179
process.availableMemory = rawMethods.availableMemory;
180180
process.kill = wrapped.kill;
181181
process.exit = wrapped.exit;
182+
process.execve = wrapped.execve;
182183
process.ref = perThreadSetup.ref;
183184
process.unref = perThreadSetup.unref;
184185

lib/internal/process/per_thread.js

+60
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ const {
1616
FunctionPrototypeCall,
1717
NumberMAX_SAFE_INTEGER,
1818
ObjectDefineProperty,
19+
ObjectEntries,
1920
ObjectFreeze,
2021
ReflectApply,
2122
RegExpPrototypeExec,
@@ -24,6 +25,7 @@ const {
2425
SetPrototypeEntries,
2526
SetPrototypeValues,
2627
StringPrototypeEndsWith,
28+
StringPrototypeIncludes,
2729
StringPrototypeReplace,
2830
StringPrototypeSlice,
2931
Symbol,
@@ -35,19 +37,26 @@ const {
3537
ErrnoException,
3638
codes: {
3739
ERR_ASSERTION,
40+
ERR_FEATURE_UNAVAILABLE_ON_PLATFORM,
3841
ERR_INVALID_ARG_TYPE,
3942
ERR_INVALID_ARG_VALUE,
4043
ERR_OUT_OF_RANGE,
4144
ERR_UNKNOWN_SIGNAL,
45+
ERR_WORKER_UNSUPPORTED_OPERATION,
4246
},
4347
} = require('internal/errors');
48+
const { emitExperimentalWarning } = require('internal/util');
4449
const format = require('internal/util/inspect').format;
4550
const {
4651
validateArray,
4752
validateNumber,
4853
validateObject,
54+
validateString,
4955
} = require('internal/validators');
5056

57+
const dc = require('diagnostics_channel');
58+
const execveDiagnosticChannel = dc.channel('process.execve');
59+
5160
const constants = internalBinding('constants').os.signals;
5261

5362
let getValidatedPath; // We need to lazy load it because of the circular dependency.
@@ -105,6 +114,7 @@ function wrapProcessMethods(binding) {
105114
rss,
106115
resourceUsage: _resourceUsage,
107116
loadEnvFile: _loadEnvFile,
117+
execve: _execve,
108118
} = binding;
109119

110120
function _rawDebug(...args) {
@@ -227,6 +237,55 @@ function wrapProcessMethods(binding) {
227237
return true;
228238
}
229239

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+
230289
const resourceValues = new Float64Array(16);
231290
function resourceUsage() {
232291
_resourceUsage(resourceValues);
@@ -271,6 +330,7 @@ function wrapProcessMethods(binding) {
271330
memoryUsage,
272331
kill,
273332
exit,
333+
execve,
274334
loadEnvFile,
275335
};
276336
}

src/node_errors.h

+5
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,11 @@
1313
#include <sstream>
1414

1515
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+
}
1621

1722
enum ErrorHandlingMode { CONTEXTIFY_ERROR, FATAL_ERROR, MODULE_ERROR };
1823
void AppendExceptionLine(Environment* env,

src/node_process_methods.cc

+99
Original file line numberDiff line numberDiff line change
@@ -27,12 +27,14 @@
2727
#if defined(_MSC_VER)
2828
#include <direct.h>
2929
#include <io.h>
30+
#include <process.h>
3031
#define umask _umask
3132
typedef int mode_t;
3233
#else
3334
#include <pthread.h>
3435
#include <sys/resource.h> // getrlimit, setrlimit
3536
#include <termios.h> // tcgetattr, tcsetattr
37+
#include <unistd.h>
3638
#endif
3739

3840
namespace node {
@@ -471,6 +473,95 @@ static void ReallyExit(const FunctionCallbackInfo<Value>& args) {
471473
env->Exit(code);
472474
}
473475

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+
474565
static void LoadEnvFile(const v8::FunctionCallbackInfo<v8::Value>& args) {
475566
Environment* env = Environment::GetCurrent(args);
476567
std::string path = ".env";
@@ -662,6 +753,10 @@ static void CreatePerIsolateProperties(IsolateData* isolate_data,
662753
SetMethodNoSideEffect(isolate, target, "cwd", Cwd);
663754
SetMethod(isolate, target, "dlopen", binding::DLOpen);
664755
SetMethod(isolate, target, "reallyExit", ReallyExit);
756+
757+
#ifdef __POSIX__
758+
SetMethod(isolate, target, "execve", Execve);
759+
#endif
665760
SetMethodNoSideEffect(isolate, target, "uptime", Uptime);
666761
SetMethod(isolate, target, "patchProcessObject", PatchProcessObject);
667762

@@ -704,6 +799,10 @@ void RegisterExternalReferences(ExternalReferenceRegistry* registry) {
704799
registry->Register(Cwd);
705800
registry->Register(binding::DLOpen);
706801
registry->Register(ReallyExit);
802+
803+
#ifdef __POSIX__
804+
registry->Register(Execve);
805+
#endif
707806
registry->Register(Uptime);
708807
registry->Register(PatchProcessObject);
709808

+26
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
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+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
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+
}

0 commit comments

Comments
 (0)