Skip to content

Commit 7e50270

Browse files
ShogunPandaRafaelGSS
authored andcommitted
process: add execve
PR-URL: #56496 Reviewed-By: Matteo Collina <[email protected]> Reviewed-By: Rafael Gonzaga <[email protected]> Reviewed-By: Bryan English <[email protected]> Reviewed-By: James M Snell <[email protected]>
1 parent 6ce3e2b commit 7e50270

14 files changed

+470
-2
lines changed

doc/api/diagnostics_channel.md

Lines changed: 9 additions & 0 deletions
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

Lines changed: 28 additions & 2 deletions
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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,7 @@ const rawMethods = internalBinding('process_methods');
180180
process.availableMemory = rawMethods.availableMemory;
181181
process.kill = wrapped.kill;
182182
process.exit = wrapped.exit;
183+
process.execve = wrapped.execve;
183184
process.ref = perThreadSetup.ref;
184185
process.unref = perThreadSetup.unref;
185186

lib/internal/process/per_thread.js

Lines changed: 60 additions & 0 deletions
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,20 +37,27 @@ 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_OPERATION_FAILED,
4144
ERR_OUT_OF_RANGE,
4245
ERR_UNKNOWN_SIGNAL,
46+
ERR_WORKER_UNSUPPORTED_OPERATION,
4347
},
4448
} = require('internal/errors');
49+
const { emitExperimentalWarning } = require('internal/util');
4550
const format = require('internal/util/inspect').format;
4651
const {
4752
validateArray,
4853
validateNumber,
4954
validateObject,
55+
validateString,
5056
} = require('internal/validators');
5157

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

5463
let getValidatedPath; // We need to lazy load it because of the circular dependency.
@@ -107,6 +116,7 @@ function wrapProcessMethods(binding) {
107116
rss,
108117
resourceUsage: _resourceUsage,
109118
loadEnvFile: _loadEnvFile,
119+
execve: _execve,
110120
} = binding;
111121

112122
function _rawDebug(...args) {
@@ -273,6 +283,55 @@ function wrapProcessMethods(binding) {
273283
return true;
274284
}
275285

286+
function execve(execPath, args, env) {
287+
emitExperimentalWarning('process.execve');
288+
289+
const { isMainThread } = require('internal/worker');
290+
291+
if (!isMainThread) {
292+
throw new ERR_WORKER_UNSUPPORTED_OPERATION('Calling process.execve');
293+
} else if (process.platform === 'win32') {
294+
throw new ERR_FEATURE_UNAVAILABLE_ON_PLATFORM('process.execve');
295+
}
296+
297+
validateString(execPath, 'execPath');
298+
validateArray(args, 'args');
299+
300+
for (let i = 0; i < args.length; i++) {
301+
const arg = args[i];
302+
if (typeof arg !== 'string' || StringPrototypeIncludes(arg, '\u0000')) {
303+
throw new ERR_INVALID_ARG_VALUE(`args[${i}]`, arg, 'must be a string without null bytes');
304+
}
305+
}
306+
307+
const envArray = [];
308+
if (env !== undefined) {
309+
validateObject(env, 'env');
310+
311+
for (const { 0: key, 1: value } of ObjectEntries(env)) {
312+
if (
313+
typeof key !== 'string' ||
314+
typeof value !== 'string' ||
315+
StringPrototypeIncludes(key, '\u0000') ||
316+
StringPrototypeIncludes(value, '\u0000')
317+
) {
318+
throw new ERR_INVALID_ARG_VALUE(
319+
'env', env, 'must be an object with string keys and values without null bytes',
320+
);
321+
} else {
322+
ArrayPrototypePush(envArray, `${key}=${value}`);
323+
}
324+
}
325+
}
326+
327+
if (execveDiagnosticChannel.hasSubscribers) {
328+
execveDiagnosticChannel.publish({ execPath, args, env: envArray });
329+
}
330+
331+
// Perform the system call
332+
_execve(execPath, args, envArray);
333+
}
334+
276335
const resourceValues = new Float64Array(16);
277336
function resourceUsage() {
278337
_resourceUsage(resourceValues);
@@ -318,6 +377,7 @@ function wrapProcessMethods(binding) {
318377
memoryUsage,
319378
kill,
320379
exit,
380+
execve,
321381
loadEnvFile,
322382
};
323383
}

src/node_errors.h

Lines changed: 5 additions & 0 deletions
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

Lines changed: 99 additions & 0 deletions
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 {
@@ -494,6 +496,95 @@ static void ReallyExit(const FunctionCallbackInfo<Value>& args) {
494496
env->Exit(code);
495497
}
496498

499+
#ifdef __POSIX__
500+
inline int persist_standard_stream(int fd) {
501+
int flags = fcntl(fd, F_GETFD, 0);
502+
503+
if (flags < 0) {
504+
return flags;
505+
}
506+
507+
flags &= ~FD_CLOEXEC;
508+
return fcntl(fd, F_SETFD, flags);
509+
}
510+
511+
static void Execve(const FunctionCallbackInfo<Value>& args) {
512+
Environment* env = Environment::GetCurrent(args);
513+
Isolate* isolate = env->isolate();
514+
Local<Context> context = env->context();
515+
516+
THROW_IF_INSUFFICIENT_PERMISSIONS(
517+
env, permission::PermissionScope::kChildProcess, "");
518+
519+
CHECK(args[0]->IsString());
520+
CHECK(args[1]->IsArray());
521+
CHECK(args[2]->IsArray());
522+
523+
Local<Array> argv_array = args[1].As<Array>();
524+
Local<Array> envp_array = args[2].As<Array>();
525+
526+
// Copy arguments and environment
527+
Utf8Value executable(isolate, args[0]);
528+
std::vector<std::string> argv_strings(argv_array->Length());
529+
std::vector<std::string> envp_strings(envp_array->Length());
530+
std::vector<char*> argv(argv_array->Length() + 1);
531+
std::vector<char*> envp(envp_array->Length() + 1);
532+
533+
for (unsigned int i = 0; i < argv_array->Length(); i++) {
534+
Local<Value> str;
535+
if (!argv_array->Get(context, i).ToLocal(&str)) {
536+
THROW_ERR_INVALID_ARG_VALUE(env, "Failed to deserialize argument.");
537+
return;
538+
}
539+
540+
argv_strings[i] = Utf8Value(isolate, str).ToString();
541+
argv[i] = argv_strings[i].data();
542+
}
543+
argv[argv_array->Length()] = nullptr;
544+
545+
for (unsigned int i = 0; i < envp_array->Length(); i++) {
546+
Local<Value> str;
547+
if (!envp_array->Get(context, i).ToLocal(&str)) {
548+
THROW_ERR_INVALID_ARG_VALUE(
549+
env, "Failed to deserialize environment variable.");
550+
return;
551+
}
552+
553+
envp_strings[i] = Utf8Value(isolate, str).ToString();
554+
envp[i] = envp_strings[i].data();
555+
}
556+
557+
envp[envp_array->Length()] = nullptr;
558+
559+
// Set stdin, stdout and stderr to be non-close-on-exec
560+
// so that the new process will inherit it.
561+
if (persist_standard_stream(0) < 0 || persist_standard_stream(1) < 0 ||
562+
persist_standard_stream(2) < 0) {
563+
env->ThrowErrnoException(errno, "fcntl");
564+
return;
565+
}
566+
567+
// Perform the execve operation.
568+
RunAtExit(env);
569+
execve(*executable, argv.data(), envp.data());
570+
571+
// If it returns, it means that the execve operation failed.
572+
// In that case we abort the process.
573+
auto error_message = std::string("process.execve failed with error code ") +
574+
errors::errno_string(errno);
575+
576+
// Abort the process
577+
Local<v8::Value> exception =
578+
ErrnoException(isolate, errno, "execve", *executable);
579+
Local<v8::Message> message = v8::Exception::CreateMessage(isolate, exception);
580+
581+
std::string info = FormatErrorMessage(
582+
isolate, context, error_message.c_str(), message, true);
583+
FPrintF(stderr, "%s\n", info);
584+
ABORT();
585+
}
586+
#endif
587+
497588
static void LoadEnvFile(const v8::FunctionCallbackInfo<v8::Value>& args) {
498589
Environment* env = Environment::GetCurrent(args);
499590
std::string path = ".env";
@@ -686,6 +777,10 @@ static void CreatePerIsolateProperties(IsolateData* isolate_data,
686777
SetMethodNoSideEffect(isolate, target, "cwd", Cwd);
687778
SetMethod(isolate, target, "dlopen", binding::DLOpen);
688779
SetMethod(isolate, target, "reallyExit", ReallyExit);
780+
781+
#ifdef __POSIX__
782+
SetMethod(isolate, target, "execve", Execve);
783+
#endif
689784
SetMethodNoSideEffect(isolate, target, "uptime", Uptime);
690785
SetMethod(isolate, target, "patchProcessObject", PatchProcessObject);
691786

@@ -729,6 +824,10 @@ void RegisterExternalReferences(ExternalReferenceRegistry* registry) {
729824
registry->Register(Cwd);
730825
registry->Register(binding::DLOpen);
731826
registry->Register(ReallyExit);
827+
828+
#ifdef __POSIX__
829+
registry->Register(Execve);
830+
#endif
732831
registry->Register(Uptime);
733832
registry->Register(PatchProcessObject);
734833

Lines changed: 26 additions & 0 deletions
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+
}
Lines changed: 18 additions & 0 deletions
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)