From 61042f2c9b5bf4180da6638c4e10d2ed876ae734 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 17 May 2026 08:53:18 +0000 Subject: [PATCH] fix(libc): correct privilege drop order and sentinel in js_os_exec Two related bugs in os.exec({uid, gid}): 1. The drop ran setuid() before setgid()/setgroups(). After setuid() to a non-root uid, the process no longer holds the capability required for setgid()/setgroups() to succeed, so the gid drop silently failed and the spawned program kept the parent's gid. Reorder: groups, then gid, then uid. 2. The "was the option supplied" check used `uid == (uint32_t)-1` and `gid == (uint32_t)-1` as the "unset" sentinel. The legal POSIX value 0xFFFFFFFF collides with this sentinel, so passing exec({uid: 0xFFFFFFFF}) silently skipped the drop. Replace with explicit uid_set / gid_set bools. No test: setuid()/setgid() require either root or capabilities and behave differently across platforms (Linux vs. *BSD vs. macOS), so a portable automated test isn't viable. Verified by reading the diff against the Linux setuid(2) and setgid(2) man pages. --- quickjs-libc.c | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/quickjs-libc.c b/quickjs-libc.c index 1fe3cf5c1..b66c0b3e2 100644 --- a/quickjs-libc.c +++ b/quickjs-libc.c @@ -3580,7 +3580,8 @@ static JSValue js_os_exec(JSContext *ctx, JSValueConst this_val, bool block_flag = true, use_path = true; static const char *std_name[3] = { "stdin", "stdout", "stderr" }; int std_fds[3]; - uint32_t uid = -1, gid = -1; + uint32_t uid = 0, gid = 0; + bool uid_set = false, gid_set = false; int ngroups = -1; gid_t groups[64]; @@ -3675,6 +3676,7 @@ static JSValue js_os_exec(JSContext *ctx, JSValueConst this_val, JS_FreeValue(ctx, val); if (ret) goto exception; + uid_set = true; } val = JS_GetPropertyStr(ctx, options, "gid"); @@ -3685,6 +3687,7 @@ static JSValue js_os_exec(JSContext *ctx, JSValueConst this_val, JS_FreeValue(ctx, val); if (ret) goto exception; + gid_set = true; } val = JS_GetPropertyStr(ctx, options, "groups"); @@ -3755,16 +3758,21 @@ static JSValue js_os_exec(JSContext *ctx, JSValueConst this_val, if (chdir(cwd) < 0) _exit(127); } + /* Drop privileges in the correct order: supplementary groups and the + primary gid must change before setuid(), because setuid(non-root) + strips the capability needed for setgroups()/setgid() to succeed. + Track "was set" with explicit bools instead of a (uint32_t)-1 + sentinel, which collided with the legitimate value 0xFFFFFFFF. */ if (ngroups != -1) { if (setgroups(ngroups, groups) < 0) _exit(127); } - if (uid != -1) { - if (setuid(uid) < 0) + if (gid_set) { + if (setgid(gid) < 0) _exit(127); } - if (gid != -1) { - if (setgid(gid) < 0) + if (uid_set) { + if (setuid(uid) < 0) _exit(127); }