Skip to content

Commit 6d14352

Browse files
tniessenRafaelGSS
authored andcommitted
src,deps: disable setuid() etc if io_uring enabled
Within Node.js, attempt to determine if libuv is using io_uring. If it is, disable process.setuid() and other user identity setters. We cannot fully prevent users from changing the process's user identity, but this should still prevent some accidental, dangerous scenarios. PR-URL: nodejs-private/node-private#528 Reviewed-By: Rafael Gonzaga <rafael.nunu@hotmail.com> CVE-ID: CVE-2024-22017
1 parent 42e659c commit 6d14352

File tree

4 files changed

+107
-2
lines changed

4 files changed

+107
-2
lines changed

deps/uv/src/unix/linux.c

+8
Original file line numberDiff line numberDiff line change
@@ -503,6 +503,14 @@ static int uv__use_io_uring(void) {
503503
}
504504

505505

506+
UV_EXTERN int uv__node_patch_is_using_io_uring(void) {
507+
// This function exists only in the modified copy of libuv in the Node.js
508+
// repository. Node.js checks if this function exists and, if it does, uses it
509+
// to determine whether libuv is using io_uring or not.
510+
return uv__use_io_uring();
511+
}
512+
513+
506514
static void uv__iou_init(int epollfd,
507515
struct uv__iou* iou,
508516
uint32_t entries,

doc/api/cli.md

+3-2
Original file line numberDiff line numberDiff line change
@@ -2896,8 +2896,9 @@ various asynchronous I/O operations.
28962896

28972897
`io_uring` is disabled by default due to security concerns. When `io_uring`
28982898
is enabled, applications must not change the user identity of the process at
2899-
runtime, neither through JavaScript functions such as [`process.setuid()`][] nor
2900-
through native addons that can invoke system functions such as [`setuid(2)`][].
2899+
runtime. In this case, JavaScript functions such as [`process.setuid()`][] are
2900+
unavailable, and native addons must not invoke system functions such as
2901+
[`setuid(2)`][].
29012902

29022903
This environment variable is implemented by a dependency of Node.js and may be
29032904
removed in future versions of Node.js. No stability guarantees are provided for

src/node_credentials.cc

+53
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
#include "env-inl.h"
2+
#include "node_errors.h"
23
#include "node_external_reference.h"
34
#include "node_internals.h"
45
#include "util-inl.h"
@@ -12,6 +13,7 @@
1213
#include <unistd.h> // setuid, getuid
1314
#endif
1415
#ifdef __linux__
16+
#include <dlfcn.h> // dlsym()
1517
#include <linux/capability.h>
1618
#include <sys/auxv.h>
1719
#include <sys/syscall.h>
@@ -231,6 +233,45 @@ static gid_t gid_by_name(Isolate* isolate, Local<Value> value) {
231233
}
232234
}
233235

236+
#ifdef __linux__
237+
extern "C" {
238+
int uv__node_patch_is_using_io_uring(void);
239+
240+
int uv__node_patch_is_using_io_uring(void) __attribute__((weak));
241+
242+
typedef int (*is_using_io_uring_fn)(void);
243+
}
244+
#endif // __linux__
245+
246+
static bool UvMightBeUsingIoUring() {
247+
#ifdef __linux__
248+
// Support for io_uring is only included in libuv 1.45.0 and later, and only
249+
// on Linux (and Android, but there it is always disabled). The patch that we
250+
// apply to libuv to work around the io_uring security issue adds a function
251+
// that tells us whether io_uring is being used. If that function is not
252+
// present, we assume that we are dynamically linking against an unpatched
253+
// version.
254+
static std::atomic<is_using_io_uring_fn> check =
255+
uv__node_patch_is_using_io_uring;
256+
if (check == nullptr) {
257+
check = reinterpret_cast<is_using_io_uring_fn>(
258+
dlsym(RTLD_DEFAULT, "uv__node_patch_is_using_io_uring"));
259+
}
260+
return uv_version() >= 0x012d00u && (check == nullptr || (*check)());
261+
#else
262+
return false;
263+
#endif
264+
}
265+
266+
static bool ThrowIfUvMightBeUsingIoUring(Environment* env, const char* fn) {
267+
if (UvMightBeUsingIoUring()) {
268+
node::THROW_ERR_INVALID_STATE(
269+
env, "%s() disabled: io_uring may be enabled. See CVE-2024-22017.", fn);
270+
return true;
271+
}
272+
return false;
273+
}
274+
234275
static void GetUid(const FunctionCallbackInfo<Value>& args) {
235276
Environment* env = Environment::GetCurrent(args);
236277
CHECK(env->has_run_bootstrapping_code());
@@ -266,6 +307,8 @@ static void SetGid(const FunctionCallbackInfo<Value>& args) {
266307
CHECK_EQ(args.Length(), 1);
267308
CHECK(args[0]->IsUint32() || args[0]->IsString());
268309

310+
if (ThrowIfUvMightBeUsingIoUring(env, "setgid")) return;
311+
269312
gid_t gid = gid_by_name(env->isolate(), args[0]);
270313

271314
if (gid == gid_not_found) {
@@ -285,6 +328,8 @@ static void SetEGid(const FunctionCallbackInfo<Value>& args) {
285328
CHECK_EQ(args.Length(), 1);
286329
CHECK(args[0]->IsUint32() || args[0]->IsString());
287330

331+
if (ThrowIfUvMightBeUsingIoUring(env, "setegid")) return;
332+
288333
gid_t gid = gid_by_name(env->isolate(), args[0]);
289334

290335
if (gid == gid_not_found) {
@@ -304,6 +349,8 @@ static void SetUid(const FunctionCallbackInfo<Value>& args) {
304349
CHECK_EQ(args.Length(), 1);
305350
CHECK(args[0]->IsUint32() || args[0]->IsString());
306351

352+
if (ThrowIfUvMightBeUsingIoUring(env, "setuid")) return;
353+
307354
uid_t uid = uid_by_name(env->isolate(), args[0]);
308355

309356
if (uid == uid_not_found) {
@@ -323,6 +370,8 @@ static void SetEUid(const FunctionCallbackInfo<Value>& args) {
323370
CHECK_EQ(args.Length(), 1);
324371
CHECK(args[0]->IsUint32() || args[0]->IsString());
325372

373+
if (ThrowIfUvMightBeUsingIoUring(env, "seteuid")) return;
374+
326375
uid_t uid = uid_by_name(env->isolate(), args[0]);
327376

328377
if (uid == uid_not_found) {
@@ -363,6 +412,8 @@ static void SetGroups(const FunctionCallbackInfo<Value>& args) {
363412
CHECK_EQ(args.Length(), 1);
364413
CHECK(args[0]->IsArray());
365414

415+
if (ThrowIfUvMightBeUsingIoUring(env, "setgroups")) return;
416+
366417
Local<Array> groups_list = args[0].As<Array>();
367418
size_t size = groups_list->Length();
368419
MaybeStackBuffer<gid_t, 64> groups(size);
@@ -394,6 +445,8 @@ static void InitGroups(const FunctionCallbackInfo<Value>& args) {
394445
CHECK(args[0]->IsUint32() || args[0]->IsString());
395446
CHECK(args[1]->IsUint32() || args[1]->IsString());
396447

448+
if (ThrowIfUvMightBeUsingIoUring(env, "initgroups")) return;
449+
397450
Utf8Value arg0(env->isolate(), args[0]);
398451
gid_t extra_group;
399452
bool must_free;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
'use strict';
2+
const common = require('../common');
3+
4+
const assert = require('node:assert');
5+
const { execFileSync } = require('node:child_process');
6+
7+
if (!common.isLinux) {
8+
common.skip('test is Linux specific');
9+
}
10+
11+
if (process.arch !== 'x64' && process.arch !== 'arm64') {
12+
common.skip('io_uring support on this architecture is uncertain');
13+
}
14+
15+
const kv = /^(\d+)\.(\d+)\.(\d+)/.exec(execFileSync('uname', ['-r'])).slice(1).map((n) => parseInt(n, 10));
16+
if (((kv[0] << 16) | (kv[1] << 8) | kv[2]) < 0x050ABA) {
17+
common.skip('io_uring is likely buggy due to old kernel');
18+
}
19+
20+
const userIdentitySetters = [
21+
['setuid', [1000]],
22+
['seteuid', [1000]],
23+
['setgid', [1000]],
24+
['setegid', [1000]],
25+
['setgroups', [[1000]]],
26+
['initgroups', ['nodeuser', 1000]],
27+
];
28+
29+
for (const [fnName, args] of userIdentitySetters) {
30+
const call = `process.${fnName}(${args.map((a) => JSON.stringify(a)).join(', ')})`;
31+
const code = `try { ${call}; } catch (err) { console.log(err); }`;
32+
33+
const stdout = execFileSync(process.execPath, ['-e', code], {
34+
encoding: 'utf8',
35+
env: { ...process.env, UV_USE_IO_URING: '1' },
36+
});
37+
38+
const msg = new RegExp(`^Error: ${fnName}\\(\\) disabled: io_uring may be enabled\\. See CVE-[X0-9]{4}-`);
39+
assert.match(stdout, msg);
40+
assert.match(stdout, /code: 'ERR_INVALID_STATE'/);
41+
42+
console.log(call, stdout.slice(0, stdout.indexOf('\n')));
43+
}

0 commit comments

Comments
 (0)