Skip to content

Commit

Permalink
src,deps: disable setuid() etc if io_uring enabled
Browse files Browse the repository at this point in the history
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#529
Reviewed-By: Rafael Gonzaga <rafael.nunu@hotmail.com>
CVE-ID: CVE-2024-22017
  • Loading branch information
tniessen authored and RafaelGSS committed Feb 13, 2024
1 parent 686da19 commit 3f6addd
Show file tree
Hide file tree
Showing 4 changed files with 107 additions and 2 deletions.
8 changes: 8 additions & 0 deletions deps/uv/src/unix/linux.c
Expand Up @@ -442,6 +442,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,
Expand Down
5 changes: 3 additions & 2 deletions doc/api/cli.md
Expand Up @@ -2747,8 +2747,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
Expand Down
53 changes: 53 additions & 0 deletions 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"
Expand All @@ -12,6 +13,7 @@
#include <unistd.h> // setuid, getuid
#endif
#ifdef __linux__
#include <dlfcn.h> // dlsym()
#include <linux/capability.h>
#include <sys/auxv.h>
#include <sys/syscall.h>
Expand Down Expand Up @@ -231,6 +233,45 @@ static gid_t gid_by_name(Isolate* isolate, Local<Value> 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<is_using_io_uring_fn> check =
uv__node_patch_is_using_io_uring;
if (check == nullptr) {
check = reinterpret_cast<is_using_io_uring_fn>(
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<Value>& args) {
Environment* env = Environment::GetCurrent(args);
CHECK(env->has_run_bootstrapping_code());
Expand Down Expand Up @@ -266,6 +307,8 @@ static void SetGid(const FunctionCallbackInfo<Value>& 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) {
Expand All @@ -285,6 +328,8 @@ static void SetEGid(const FunctionCallbackInfo<Value>& 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) {
Expand All @@ -304,6 +349,8 @@ static void SetUid(const FunctionCallbackInfo<Value>& 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) {
Expand All @@ -323,6 +370,8 @@ static void SetEUid(const FunctionCallbackInfo<Value>& 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) {
Expand Down Expand Up @@ -363,6 +412,8 @@ static void SetGroups(const FunctionCallbackInfo<Value>& args) {
CHECK_EQ(args.Length(), 1);
CHECK(args[0]->IsArray());

if (ThrowIfUvMightBeUsingIoUring(env, "setgroups")) return;

Local<Array> groups_list = args[0].As<Array>();
size_t size = groups_list->Length();
MaybeStackBuffer<gid_t, 64> groups(size);
Expand Down Expand Up @@ -394,6 +445,8 @@ static void InitGroups(const FunctionCallbackInfo<Value>& 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;
Expand Down
43 changes: 43 additions & 0 deletions 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')));
}

0 comments on commit 3f6addd

Please sign in to comment.