From fc2454f29c69a23f0fa4262819437e379a4a78a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Nie=C3=9Fen?= Date: Mon, 9 Oct 2023 08:10:00 +0000 Subject: [PATCH] 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: https://github.com/nodejs-private/node-private/pull/528 Reviewed-By: Rafael Gonzaga CVE-ID: CVE-2024-22017 --- deps/uv/src/unix/linux.c | 8 +++ doc/api/cli.md | 5 +- src/node_credentials.cc | 53 +++++++++++++++++++ test/parallel/test-process-setuid-io-uring.js | 43 +++++++++++++++ 4 files changed, 107 insertions(+), 2 deletions(-) create mode 100644 test/parallel/test-process-setuid-io-uring.js diff --git a/deps/uv/src/unix/linux.c b/deps/uv/src/unix/linux.c index a693aad9a77803..29f19ecee08483 100644 --- a/deps/uv/src/unix/linux.c +++ b/deps/uv/src/unix/linux.c @@ -503,6 +503,14 @@ static int uv__use_io_uring(void) { } +UV_EXTERN int uv__node_patch_is_using_io_uring(void) { + // This function exists only in the modified copy of libuv in the Node.js + // repository. Node.js checks if this function exists and, if it does, uses it + // to determine whether libuv is using io_uring or not. + return uv__use_io_uring(); +} + + static void uv__iou_init(int epollfd, struct uv__iou* iou, uint32_t entries, diff --git a/doc/api/cli.md b/doc/api/cli.md index 27cc17e992c562..b982b94f07306b 100644 --- a/doc/api/cli.md +++ b/doc/api/cli.md @@ -2864,8 +2864,9 @@ various asynchronous I/O operations. `io_uring` is disabled by default due to security concerns. When `io_uring` is enabled, applications must not change the user identity of the process at -runtime, neither through JavaScript functions such as [`process.setuid()`][] nor -through native addons that can invoke system functions such as [`setuid(2)`][]. +runtime. In this case, JavaScript functions such as [`process.setuid()`][] are +unavailable, and native addons must not invoke system functions such as +[`setuid(2)`][]. This environment variable is implemented by a dependency of Node.js and may be removed in future versions of Node.js. No stability guarantees are provided for diff --git a/src/node_credentials.cc b/src/node_credentials.cc index b9f469f9d340ce..97cb5745a142bb 100644 --- a/src/node_credentials.cc +++ b/src/node_credentials.cc @@ -1,4 +1,5 @@ #include "env-inl.h" +#include "node_errors.h" #include "node_external_reference.h" #include "node_internals.h" #include "util-inl.h" @@ -12,6 +13,7 @@ #include // setuid, getuid #endif #ifdef __linux__ +#include // dlsym() #include #include #include @@ -231,6 +233,45 @@ static gid_t gid_by_name(Isolate* isolate, Local value) { } } +#ifdef __linux__ +extern "C" { +int uv__node_patch_is_using_io_uring(void); + +int uv__node_patch_is_using_io_uring(void) __attribute__((weak)); + +typedef int (*is_using_io_uring_fn)(void); +} +#endif // __linux__ + +static bool UvMightBeUsingIoUring() { +#ifdef __linux__ + // Support for io_uring is only included in libuv 1.45.0 and later, and only + // on Linux (and Android, but there it is always disabled). The patch that we + // apply to libuv to work around the io_uring security issue adds a function + // that tells us whether io_uring is being used. If that function is not + // present, we assume that we are dynamically linking against an unpatched + // version. + static std::atomic check = + uv__node_patch_is_using_io_uring; + if (check == nullptr) { + check = reinterpret_cast( + dlsym(RTLD_DEFAULT, "uv__node_patch_is_using_io_uring")); + } + return uv_version() >= 0x012d00u && (check == nullptr || (*check)()); +#else + return false; +#endif +} + +static bool ThrowIfUvMightBeUsingIoUring(Environment* env, const char* fn) { + if (UvMightBeUsingIoUring()) { + node::THROW_ERR_INVALID_STATE( + env, "%s() disabled: io_uring may be enabled. See CVE-2024-22017.", fn); + return true; + } + return false; +} + static void GetUid(const FunctionCallbackInfo& args) { Environment* env = Environment::GetCurrent(args); CHECK(env->has_run_bootstrapping_code()); @@ -266,6 +307,8 @@ static void SetGid(const FunctionCallbackInfo& args) { CHECK_EQ(args.Length(), 1); CHECK(args[0]->IsUint32() || args[0]->IsString()); + if (ThrowIfUvMightBeUsingIoUring(env, "setgid")) return; + gid_t gid = gid_by_name(env->isolate(), args[0]); if (gid == gid_not_found) { @@ -285,6 +328,8 @@ static void SetEGid(const FunctionCallbackInfo& args) { CHECK_EQ(args.Length(), 1); CHECK(args[0]->IsUint32() || args[0]->IsString()); + if (ThrowIfUvMightBeUsingIoUring(env, "setegid")) return; + gid_t gid = gid_by_name(env->isolate(), args[0]); if (gid == gid_not_found) { @@ -304,6 +349,8 @@ static void SetUid(const FunctionCallbackInfo& args) { CHECK_EQ(args.Length(), 1); CHECK(args[0]->IsUint32() || args[0]->IsString()); + if (ThrowIfUvMightBeUsingIoUring(env, "setuid")) return; + uid_t uid = uid_by_name(env->isolate(), args[0]); if (uid == uid_not_found) { @@ -323,6 +370,8 @@ static void SetEUid(const FunctionCallbackInfo& args) { CHECK_EQ(args.Length(), 1); CHECK(args[0]->IsUint32() || args[0]->IsString()); + if (ThrowIfUvMightBeUsingIoUring(env, "seteuid")) return; + uid_t uid = uid_by_name(env->isolate(), args[0]); if (uid == uid_not_found) { @@ -363,6 +412,8 @@ static void SetGroups(const FunctionCallbackInfo& args) { CHECK_EQ(args.Length(), 1); CHECK(args[0]->IsArray()); + if (ThrowIfUvMightBeUsingIoUring(env, "setgroups")) return; + Local groups_list = args[0].As(); size_t size = groups_list->Length(); MaybeStackBuffer groups(size); @@ -394,6 +445,8 @@ static void InitGroups(const FunctionCallbackInfo& args) { CHECK(args[0]->IsUint32() || args[0]->IsString()); CHECK(args[1]->IsUint32() || args[1]->IsString()); + if (ThrowIfUvMightBeUsingIoUring(env, "initgroups")) return; + Utf8Value arg0(env->isolate(), args[0]); gid_t extra_group; bool must_free; diff --git a/test/parallel/test-process-setuid-io-uring.js b/test/parallel/test-process-setuid-io-uring.js new file mode 100644 index 00000000000000..93193ac2f8ab99 --- /dev/null +++ b/test/parallel/test-process-setuid-io-uring.js @@ -0,0 +1,43 @@ +'use strict'; +const common = require('../common'); + +const assert = require('node:assert'); +const { execFileSync } = require('node:child_process'); + +if (!common.isLinux) { + common.skip('test is Linux specific'); +} + +if (process.arch !== 'x64' && process.arch !== 'arm64') { + common.skip('io_uring support on this architecture is uncertain'); +} + +const kv = /^(\d+)\.(\d+)\.(\d+)/.exec(execFileSync('uname', ['-r'])).slice(1).map((n) => parseInt(n, 10)); +if (((kv[0] << 16) | (kv[1] << 8) | kv[2]) < 0x050ABA) { + common.skip('io_uring is likely buggy due to old kernel'); +} + +const userIdentitySetters = [ + ['setuid', [1000]], + ['seteuid', [1000]], + ['setgid', [1000]], + ['setegid', [1000]], + ['setgroups', [[1000]]], + ['initgroups', ['nodeuser', 1000]], +]; + +for (const [fnName, args] of userIdentitySetters) { + const call = `process.${fnName}(${args.map((a) => JSON.stringify(a)).join(', ')})`; + const code = `try { ${call}; } catch (err) { console.log(err); }`; + + const stdout = execFileSync(process.execPath, ['-e', code], { + encoding: 'utf8', + env: { ...process.env, UV_USE_IO_URING: '1' }, + }); + + const msg = new RegExp(`^Error: ${fnName}\\(\\) disabled: io_uring may be enabled\\. See CVE-[X0-9]{4}-`); + assert.match(stdout, msg); + assert.match(stdout, /code: 'ERR_INVALID_STATE'/); + + console.log(call, stdout.slice(0, stdout.indexOf('\n'))); +}