Skip to content

Commit 13a9d4c

Browse files
committed
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 1b5b019 commit 13a9d4c

14 files changed

+470
-2
lines changed

doc/api/diagnostics_channel.md

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

13201320
Emitted when a new process is created.
13211321

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

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

doc/api/process.md

+28-2
Original file line numberDiff line numberDiff line change
@@ -2519,8 +2519,7 @@ if (process.getuid) {
25192519
}
25202520
```
25212521
2522-
This function is only available on POSIX platforms (i.e. not Windows or
2523-
Android).
2522+
This function not available on Windows.
25242523
25252524
## `process.hasUncaughtExceptionCaptureCallback()`
25262525
@@ -3346,6 +3345,33 @@ In custom builds from non-release versions of the source tree, only the
33463345
`name` property may be present. The additional properties should not be
33473346
relied upon to exist.
33483347
3348+
## `process.execve(file[, args[, env]])`
3349+
3350+
<!-- YAML
3351+
added: REPLACEME
3352+
-->
3353+
3354+
> Stability: 1 - Experimental
3355+
3356+
* `file` {string} The name or path of the executable file to run.
3357+
* `args` {string\[]} List of string arguments. No argument can contain a null-byte (`\u0000`).
3358+
* `env` {Object} Environment key-value pairs.
3359+
No key or value can contain a null-byte (`\u0000`).
3360+
**Default:** `process.env`.
3361+
3362+
Replaces the current process with a new process.
3363+
3364+
This is achieved by using the `execve` POSIX function and therefore no memory or other
3365+
resources from the current process are preserved, except for the standard input,
3366+
standard output and standard error file descriptor.
3367+
3368+
All other resources are discarded by the system when the processes are swapped, without triggering
3369+
any exit or close events and without running any cleanup handler.
3370+
3371+
This function will never return, unless an error occurred.
3372+
3373+
This function is only available on POSIX platforms (i.e. not Windows or Android).
3374+
33493375
## `process.report`
33503376
33513377
<!-- YAML

lib/internal/bootstrap/node.js

+1
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,7 @@ const rawMethods = internalBinding('process_methods');
177177
process.availableMemory = rawMethods.availableMemory;
178178
process.kill = wrapped.kill;
179179
process.exit = wrapped.exit;
180+
process.execve = wrapped.execve;
180181
process.ref = perThreadSetup.ref;
181182
process.unref = perThreadSetup.unref;
182183

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,
@@ -34,20 +36,27 @@ const {
3436
const {
3537
ErrnoException,
3638
codes: {
39+
ERR_FEATURE_UNAVAILABLE_ON_PLATFORM,
3740
ERR_INVALID_ARG_TYPE,
3841
ERR_INVALID_ARG_VALUE,
3942
ERR_OPERATION_FAILED,
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.
@@ -103,6 +112,7 @@ function wrapProcessMethods(binding) {
103112
rss,
104113
resourceUsage: _resourceUsage,
105114
loadEnvFile: _loadEnvFile,
115+
execve: _execve,
106116
} = binding;
107117

108118
function _rawDebug(...args) {
@@ -269,6 +279,55 @@ function wrapProcessMethods(binding) {
269279
return true;
270280
}
271281

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+
272331
const resourceValues = new Float64Array(16);
273332
function resourceUsage() {
274333
_resourceUsage(resourceValues);
@@ -314,6 +373,7 @@ function wrapProcessMethods(binding) {
314373
memoryUsage,
315374
kill,
316375
exit,
376+
execve,
317377
loadEnvFile,
318378
};
319379
}

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 {
@@ -495,6 +497,95 @@ static void ReallyExit(const FunctionCallbackInfo<Value>& args) {
495497
env->Exit(code);
496498
}
497499

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+
498589
static void LoadEnvFile(const v8::FunctionCallbackInfo<v8::Value>& args) {
499590
Environment* env = Environment::GetCurrent(args);
500591
std::string path = ".env";
@@ -687,6 +778,10 @@ static void CreatePerIsolateProperties(IsolateData* isolate_data,
687778
SetMethodNoSideEffect(isolate, target, "cwd", Cwd);
688779
SetMethod(isolate, target, "dlopen", binding::DLOpen);
689780
SetMethod(isolate, target, "reallyExit", ReallyExit);
781+
782+
#ifdef __POSIX__
783+
SetMethod(isolate, target, "execve", Execve);
784+
#endif
690785
SetMethodNoSideEffect(isolate, target, "uptime", Uptime);
691786
SetMethod(isolate, target, "patchProcessObject", PatchProcessObject);
692787

@@ -730,6 +825,10 @@ void RegisterExternalReferences(ExternalReferenceRegistry* registry) {
730825
registry->Register(Cwd);
731826
registry->Register(binding::DLOpen);
732827
registry->Register(ReallyExit);
828+
829+
#ifdef __POSIX__
830+
registry->Register(Execve);
831+
#endif
733832
registry->Register(Uptime);
734833
registry->Register(PatchProcessObject);
735834

+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)