Skip to content

fix(ext/node): pass uid/gid to spawn and implement process.getgroups#32772

Merged
bartlomieju merged 6 commits intodenoland:mainfrom
fraidev:fix/child-process-uid-gid
Mar 20, 2026
Merged

fix(ext/node): pass uid/gid to spawn and implement process.getgroups#32772
bartlomieju merged 6 commits intodenoland:mainfrom
fraidev:fix/child-process-uid-gid

Conversation

@fraidev
Copy link
Copy Markdown
Contributor

@fraidev fraidev commented Mar 16, 2026

Two fixes for child_process uid/gid support:

  1. uid and gid options were destructured but not passed to Deno.Command in async spawn() (they were already passed in spawnSync). Also converts PermissionDenied errors to EPERM and throws synchronously, matching Node.js behavior.
  2. Implements process.getgroups() using libc getgroups() syscall, which was missing entirely.

@fraidev fraidev added the ci-draft Run the CI on draft PRs. label Mar 16, 2026
fraidev added 2 commits March 16, 2026 13:22
Two fixes for child_process uid/gid support:

1. `uid` and `gid` options were destructured but not passed to
   `Deno.Command` in async `spawn()` (they were already passed in
   `spawnSync`). Also converts `PermissionDenied` errors to `EPERM`
   and throws synchronously, matching Node.js behavior.

2. Implements `process.getgroups()` using libc `getgroups()` syscall,
   which was missing entirely.
@fraidev fraidev force-pushed the fix/child-process-uid-gid branch from 1440016 to 2518e47 Compare March 16, 2026 16:28
@fraidev fraidev marked this pull request as ready for review March 16, 2026 17:38
@fraidev fraidev removed the ci-draft Run the CI on draft PRs. label Mar 16, 2026
Copy link
Copy Markdown
Contributor

@kajukitli kajukitli left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The PR adds os.getgroups() functionality for Node.js compatibility. The implementation has a critical type safety issue where a Vec<u32> buffer is cast to *mut libc::gid_t, but gid_t may not be 32 bits on all Unix platforms, potentially causing memory corruption. There's also a race condition between the two getgroups calls and silent error handling that could hide system errors.

[HIGH] ext/node/ops/os/mod.rs:269: Type mismatch when casting u32 buffer to libc::gid_t pointer. On some Unix platforms, libc::gid_t may be a different size than u32 (e.g., u16 or u64). The code allocates Vec but casts to *mut libc::gid_t, which could cause memory corruption or incorrect values.

Suggestion: Use Veclibc::gid_t for the buffer, then convert to u32 when returning: let mut groups: Vec<libc::gid_t> = vec![0; ngroups as usize]; followed by Ok(groups.iter().map(|&g| g as u32).collect())

[MEDIUM] ext/node/ops/os/mod.rs:265: Race condition: The number of groups can change between the first getgroups(0, null) call and the second call with the buffer. If groups increase, the second call may fail or truncate results.

Suggestion: Consider using a retry loop if the second call indicates more groups than expected, or allocate a larger buffer upfront.

[LOW] ext/node/ops/os/mod.rs:263: Silently returning empty vector on getgroups error hides legitimate system errors from the caller.

Suggestion: Consider returning an error when ngroups < 0 from the first call instead of silently returning an empty vector.

- Use Vec<libc::gid_t> instead of Vec<u32> for the buffer to avoid
  type mismatch on platforms where gid_t is not u32
- Return errors instead of silently returning empty vec when
  getgroups syscall fails
- Change return type from PermissionCheckError to OsError to
  support reporting IO errors

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

@kajukitli kajukitli left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Two verified findings: a critical bug in the PermissionDenied error handler that breaks the error handling flow by using throw instead of reassigning e, and a medium-severity inconsistency where process.getgroups is set to undefined on Windows instead of being deleted like other process identity methods.

[HIGH] ext/node/polyfills/internal/child_process.ts:583: PermissionDenied handler uses throw instead of reassigning e, breaking the error handling flow. When Deno.errors.NotFound is caught, the code reassigns e = _createSpawnError(...) and falls through to set up stdio streams (stdin/stdout/stderr pipes) before emitting the error asynchronously via queueMicrotask. The new PermissionDenied handler instead does throw _createSpawnError(...), which skips all stdio pipe setup and the deferred error emission. This means the ChildProcess will have null stdin/stdout/stderr when uid/gid permission errors occur, and the error won't be emitted via the normal 'error' event pathway — it will throw synchronously instead.

Suggestion: Change throw _createSpawnError("EPERM", command, args.slice(1)); to e = _createSpawnError("EPERM", command, args.slice(1)); to match the pattern used for the ENOENT case, allowing the catch block to fall through and properly set up stdio streams and emit the error asynchronously.

[MEDIUM] ext/node/polyfills/process.ts:859: process.getgroups is set to undefined on Windows via a ternary, meaning 'getgroups' in process returns true and process.getgroups is undefined. This is inconsistent with both Node.js (where the property doesn't exist on Windows) and the other process identity methods in this file (getgid, getuid, getegid, geteuid) which are assigned unconditionally then deleted on Windows around line 1044-1048.

Suggestion: Assign process.getgroups unconditionally as () => op_getgroups(), then add delete process.getgroups; in the if (isWindows) block alongside the other deletions (around line 1044-1048).

bartlomieju and others added 2 commits March 20, 2026 17:04
- Change `throw` to `e =` in PermissionDenied catch handler so the
  error flows through existing stdio setup and #_handleError path
- Assign process.getgroups unconditionally and delete on Windows,
  matching getgid/getuid/getegid/geteuid pattern
- Use EPERM instead of ENOTSUP for uid/gid on Windows

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Node.js expects ENOTSUP on Windows per test-child-process-uid-gid.js.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@bartlomieju bartlomieju enabled auto-merge (squash) March 20, 2026 16:06
Node.js throws EPERM synchronously from spawn() for uid/gid permission
errors (unlike ENOENT which is emitted as an error event). The throw
is needed for assert.throws() in test-child-process-uid-gid.js to work.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@bartlomieju bartlomieju merged commit 49a9e13 into denoland:main Mar 20, 2026
220 of 222 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants