Skip to content

Commit 0a8cce4

Browse files
authored
Revert "fix(ext/node): support numeric FDs in child_process stdio array (#32959)" (#33017)
## Summary Reverts #32959 (commit ffbe236) which added support for numeric FDs in `child_process` stdio array. The reverted commit changed deserialization of numeric values in stdio config from `Rid(ResourceId)` to `Fd(i32)`, meaning all numeric stdio values are now interpreted as raw OS file descriptors instead of Deno resource IDs. This is a breaking change because Deno's Node-compat `fs.openSync` currently returns Deno resource IDs, not raw file descriptors (unlike Node.js which returns raw FDs). Any code that passes a numeric value from `fs.openSync` (or similar) as a stdio option now calls `dup()` on a wrong/nonexistent OS file descriptor instead of looking it up in Deno's resource table. This was reported as breaking Claude Code installed via Deno -- it could not run Bash at all. #31965 needs to land first (making `fs.openSync` return raw FDs), and then #32959 can be re-landed safely.
1 parent 47b20af commit 0a8cce4

File tree

2 files changed

+70
-165
lines changed

2 files changed

+70
-165
lines changed

ext/process/lib.rs

Lines changed: 70 additions & 132 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,6 @@ use std::collections::HashMap;
77
use std::ffi::OsString;
88
use std::io::Write;
99
#[cfg(unix)]
10-
use std::os::fd::FromRawFd;
11-
#[cfg(unix)]
1210
use std::os::unix::prelude::ExitStatusExt;
1311
#[cfg(unix)]
1412
use std::os::unix::process::CommandExt;
@@ -81,14 +79,12 @@ impl Stdio {
8179
}
8280

8381
#[derive(Copy, Clone, Eq, PartialEq)]
84-
pub enum StdioOrFdOrRid {
82+
pub enum StdioOrRid {
8583
Stdio(Stdio),
86-
/// A raw file descriptor (e.g. from Node's `fs.openSync`).
87-
Fd(i32),
8884
Rid(ResourceId),
8985
}
9086

91-
impl<'de> Deserialize<'de> for StdioOrFdOrRid {
87+
impl<'de> Deserialize<'de> for StdioOrRid {
9288
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
9389
where
9490
D: serde::Deserializer<'de>,
@@ -97,69 +93,38 @@ impl<'de> Deserialize<'de> for StdioOrFdOrRid {
9793
let value = Value::deserialize(deserializer)?;
9894
match value {
9995
Value::String(val) => match val.as_str() {
100-
"inherit" => Ok(StdioOrFdOrRid::Stdio(Stdio::Inherit)),
101-
"piped" => Ok(StdioOrFdOrRid::Stdio(Stdio::Piped)),
102-
"null" => Ok(StdioOrFdOrRid::Stdio(Stdio::Null)),
96+
"inherit" => Ok(StdioOrRid::Stdio(Stdio::Inherit)),
97+
"piped" => Ok(StdioOrRid::Stdio(Stdio::Piped)),
98+
"null" => Ok(StdioOrRid::Stdio(Stdio::Null)),
10399
"ipc_for_internal_use" => {
104-
Ok(StdioOrFdOrRid::Stdio(Stdio::IpcForInternalUse))
100+
Ok(StdioOrRid::Stdio(Stdio::IpcForInternalUse))
105101
}
106102
val => Err(serde::de::Error::unknown_variant(
107103
val,
108104
&["inherit", "piped", "null"],
109105
)),
110106
},
111-
Value::Number(val) => match val.as_i64() {
112-
Some(val) if val >= 0 && val <= i32::MAX as i64 => {
113-
Ok(StdioOrFdOrRid::Fd(val as i32))
107+
Value::Number(val) => match val.as_u64() {
108+
Some(val) if val <= ResourceId::MAX as u64 => {
109+
Ok(StdioOrRid::Rid(val as ResourceId))
114110
}
115-
_ => Err(serde::de::Error::custom("Expected a non-negative integer")),
111+
_ => Err(serde::de::Error::custom("Expected a positive integer")),
116112
},
117113
_ => Err(serde::de::Error::custom(
118-
r#"Expected a file descriptor, "inherit", "piped", or "null""#,
114+
r#"Expected a resource id, "inherit", "piped", or "null""#,
119115
)),
120116
}
121117
}
122118
}
123119

124-
impl StdioOrFdOrRid {
120+
impl StdioOrRid {
125121
pub fn as_stdio(
126122
&self,
127123
state: &mut OpState,
128124
) -> Result<StdStdio, ProcessError> {
129125
match &self {
130-
StdioOrFdOrRid::Stdio(val) => Ok(val.as_stdio()),
131-
StdioOrFdOrRid::Fd(fd) => {
132-
// Convert a raw FD to a Stdio by dup'ing it into a std::fs::File.
133-
#[cfg(unix)]
134-
{
135-
// SAFETY: libc::dup is safe to call with any fd; returns -1 on error.
136-
let duped = unsafe { libc::dup(*fd) };
137-
if duped < 0 {
138-
return Err(ProcessError::Io(std::io::Error::last_os_error()));
139-
}
140-
// SAFETY: duped is a valid fd we own (just created by dup).
141-
Ok(unsafe { std::fs::File::from_raw_fd(duped) }.into())
142-
}
143-
#[cfg(windows)]
144-
{
145-
// SAFETY: get_osfhandle converts a CRT fd to a Windows HANDLE; returns -1 on error.
146-
let handle = unsafe { libc::get_osfhandle(*fd) };
147-
if handle == -1 {
148-
return Err(ProcessError::Io(std::io::Error::last_os_error()));
149-
}
150-
// SAFETY: handle is a valid Windows HANDLE (verified non-negative above).
151-
// BorrowedHandle does not take ownership.
152-
let borrowed = unsafe {
153-
std::os::windows::io::BorrowedHandle::borrow_raw(
154-
handle as *mut std::ffi::c_void,
155-
)
156-
};
157-
let owned =
158-
borrowed.try_clone_to_owned().map_err(ProcessError::Io)?;
159-
Ok(std::fs::File::from(owned).into())
160-
}
161-
}
162-
StdioOrFdOrRid::Rid(rid) => {
126+
StdioOrRid::Stdio(val) => Ok(val.as_stdio()),
127+
StdioOrRid::Rid(rid) => {
163128
Ok(FileResource::with_file(state, *rid, |file| {
164129
file.as_stdio().map_err(deno_error::JsErrorBox::from_err)
165130
})?)
@@ -168,7 +133,7 @@ impl StdioOrFdOrRid {
168133
}
169134

170135
pub fn is_ipc(&self) -> bool {
171-
matches!(self, StdioOrFdOrRid::Stdio(Stdio::IpcForInternalUse))
136+
matches!(self, StdioOrRid::Stdio(Stdio::IpcForInternalUse))
172137
}
173138
}
174139

@@ -283,7 +248,7 @@ pub struct SpawnArgs {
283248

284249
input: Option<JsBuffer>,
285250

286-
extra_stdio: Vec<StdioOrFdOrRid>,
251+
extra_stdio: Vec<Stdio>,
287252
detached: bool,
288253
needs_npm_process_state: bool,
289254
#[cfg(unix)]
@@ -384,9 +349,9 @@ pub enum ProcessError {
384349
#[derive(Deserialize)]
385350
#[serde(rename_all = "camelCase")]
386351
pub struct ChildStdio {
387-
stdin: StdioOrFdOrRid,
388-
stdout: StdioOrFdOrRid,
389-
stderr: StdioOrFdOrRid,
352+
stdin: StdioOrRid,
353+
stdout: StdioOrRid,
354+
stderr: StdioOrRid,
390355
}
391356

392357
#[derive(ToV8)]
@@ -553,15 +518,11 @@ fn create_command(
553518
}
554519

555520
command.stdout(match args.stdio.stdout {
556-
StdioOrFdOrRid::Stdio(Stdio::Inherit) => {
557-
StdioOrFdOrRid::Rid(1).as_stdio(state)?
558-
}
521+
StdioOrRid::Stdio(Stdio::Inherit) => StdioOrRid::Rid(1).as_stdio(state)?,
559522
value => value.as_stdio(state)?,
560523
});
561524
command.stderr(match args.stdio.stderr {
562-
StdioOrFdOrRid::Stdio(Stdio::Inherit) => {
563-
StdioOrFdOrRid::Rid(2).as_stdio(state)?
564-
}
525+
StdioOrRid::Stdio(Stdio::Inherit) => StdioOrRid::Rid(2).as_stdio(state)?,
565526
value => value.as_stdio(state)?,
566527
});
567528

@@ -617,36 +578,26 @@ fn create_command(
617578
// index 0 in `extra_stdio` actually refers to fd 3
618579
// because we handle stdin,stdout,stderr specially
619580
let fd = (i + 3) as i32;
620-
match stdio {
621-
StdioOrFdOrRid::Stdio(Stdio::Piped) => {
622-
let (fd1, fd2) = deno_io::bi_pipe_pair_raw()?;
623-
fds_to_dup.push((fd2, fd));
624-
fds_to_close.push(fd2);
625-
let rid = state.resource_table.add(
626-
match deno_io::BiPipeResource::from_raw_handle(fd1) {
627-
Ok(v) => v,
628-
Err(e) => {
629-
log::warn!(
630-
"Failed to open bidirectional pipe for fd {fd}: {e}"
631-
);
632-
extra_pipe_rids.push(None);
633-
continue;
634-
}
635-
},
636-
);
637-
extra_pipe_rids.push(Some(rid));
638-
}
639-
StdioOrFdOrRid::Fd(raw_fd) => {
640-
// Raw file descriptor (from fs.openSync etc.)
641-
// dup2 it to the target fd in the child process.
642-
// The caller retains ownership of raw_fd so we don't
643-
// add it to fds_to_close.
644-
fds_to_dup.push((raw_fd, fd));
645-
extra_pipe_rids.push(None);
646-
}
647-
_ => {
648-
extra_pipe_rids.push(None);
649-
}
581+
// TODO(nathanwhit): handle inherited, but this relies on the parent process having
582+
// fds open already. since we don't generally support dealing with raw fds,
583+
// we can't properly support this
584+
if matches!(stdio, Stdio::Piped) {
585+
let (fd1, fd2) = deno_io::bi_pipe_pair_raw()?;
586+
fds_to_dup.push((fd2, fd));
587+
fds_to_close.push(fd2);
588+
let rid = state.resource_table.add(
589+
match deno_io::BiPipeResource::from_raw_handle(fd1) {
590+
Ok(v) => v,
591+
Err(e) => {
592+
log::warn!("Failed to open bidirectional pipe for fd {fd}: {e}");
593+
extra_pipe_rids.push(None);
594+
continue;
595+
}
596+
},
597+
);
598+
extra_pipe_rids.push(Some(rid));
599+
} else {
600+
extra_pipe_rids.push(None);
650601
}
651602
}
652603

@@ -719,41 +670,28 @@ fn create_command(
719670
// index 0 in `extra_stdio` actually refers to fd 3
720671
// because we handle stdin,stdout,stderr specially
721672
let fd = (i + 3) as i32;
722-
match stdio {
723-
StdioOrFdOrRid::Stdio(Stdio::Piped) => {
724-
let (fd1, fd2) = deno_io::bi_pipe_pair_raw()?;
725-
handles_to_close.push(fd2);
726-
let rid = state.resource_table.add(
727-
match deno_io::BiPipeResource::from_raw_handle(fd1) {
728-
Ok(v) => v,
729-
Err(e) => {
730-
log::warn!(
731-
"Failed to open bidirectional pipe for fd {fd}: {e}"
732-
);
733-
extra_pipe_rids.push(None);
734-
continue;
735-
}
736-
},
737-
);
738-
command.extra_handle(Some(fd2));
739-
extra_pipe_rids.push(Some(rid));
740-
}
741-
StdioOrFdOrRid::Fd(raw_fd) => {
742-
// SAFETY: get_osfhandle converts a CRT fd to a Windows HANDLE; returns -1 on error.
743-
let handle = unsafe { libc::get_osfhandle(raw_fd) };
744-
if handle != -1 {
745-
command.extra_handle(Some(handle as _));
746-
} else {
747-
command.extra_handle(None);
748-
}
749-
extra_pipe_rids.push(None);
750-
}
751-
_ => {
752-
// No handle -- push an empty slot so subsequent handles
753-
// get the right fd indices.
754-
command.extra_handle(None);
755-
extra_pipe_rids.push(None);
756-
}
673+
// TODO(nathanwhit): handle inherited, but this relies on the parent process having
674+
// fds open already. since we don't generally support dealing with raw fds,
675+
// we can't properly support this
676+
if matches!(stdio, Stdio::Piped) {
677+
let (fd1, fd2) = deno_io::bi_pipe_pair_raw()?;
678+
handles_to_close.push(fd2);
679+
let rid = state.resource_table.add(
680+
match deno_io::BiPipeResource::from_raw_handle(fd1) {
681+
Ok(v) => v,
682+
Err(e) => {
683+
log::warn!("Failed to open bidirectional pipe for fd {fd}: {e}");
684+
extra_pipe_rids.push(None);
685+
continue;
686+
}
687+
},
688+
);
689+
command.extra_handle(Some(fd2));
690+
extra_pipe_rids.push(Some(rid));
691+
} else {
692+
// no handle, push an empty handle so we need get the right fds for following handles
693+
command.extra_handle(None);
694+
extra_pipe_rids.push(None);
757695
}
758696
}
759697

@@ -1177,8 +1115,8 @@ fn op_spawn_sync(
11771115
state: &mut OpState,
11781116
#[serde] args: SpawnArgs,
11791117
) -> Result<SpawnOutput, ProcessError> {
1180-
let stdout = matches!(args.stdio.stdout, StdioOrFdOrRid::Stdio(Stdio::Piped));
1181-
let stderr = matches!(args.stdio.stderr, StdioOrFdOrRid::Stdio(Stdio::Piped));
1118+
let stdout = matches!(args.stdio.stdout, StdioOrRid::Stdio(Stdio::Piped));
1119+
let stderr = matches!(args.stdio.stderr, StdioOrRid::Stdio(Stdio::Piped));
11821120
let input = args.input.clone();
11831121
let (mut command, _, _, _) =
11841122
create_command(state, args, "Deno.Command().outputSync()")?;
@@ -1275,11 +1213,11 @@ mod deprecated {
12751213
cwd: Option<String>,
12761214
env: Vec<(String, String)>,
12771215
#[from_v8(serde)]
1278-
stdin: StdioOrFdOrRid,
1216+
stdin: StdioOrRid,
12791217
#[from_v8(serde)]
1280-
stdout: StdioOrFdOrRid,
1218+
stdout: StdioOrRid,
12811219
#[from_v8(serde)]
1282-
stderr: StdioOrFdOrRid,
1220+
stderr: StdioOrRid,
12831221
}
12841222

12851223
struct ChildResource {
@@ -1355,14 +1293,14 @@ mod deprecated {
13551293
c.stdin(run_args.stdin.as_stdio(state)?);
13561294
c.stdout(
13571295
match run_args.stdout {
1358-
StdioOrFdOrRid::Stdio(Stdio::Inherit) => StdioOrFdOrRid::Rid(1),
1296+
StdioOrRid::Stdio(Stdio::Inherit) => StdioOrRid::Rid(1),
13591297
value => value,
13601298
}
13611299
.as_stdio(state)?,
13621300
);
13631301
c.stderr(
13641302
match run_args.stderr {
1365-
StdioOrFdOrRid::Stdio(Stdio::Inherit) => StdioOrFdOrRid::Rid(2),
1303+
StdioOrRid::Stdio(Stdio::Inherit) => StdioOrRid::Rid(2),
13661304
value => value,
13671305
}
13681306
.as_stdio(state)?,

tests/unit_node/child_process_test.ts

Lines changed: 0 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22

33
import CP from "node:child_process";
44
import { Buffer } from "node:buffer";
5-
import * as fs from "node:fs";
65
import {
76
assert,
87
assertEquals,
@@ -1345,35 +1344,3 @@ Deno.test(async function stdoutWriteMultipleChunksNotTruncated() {
13451344
await Deno.remove(script);
13461345
}
13471346
});
1348-
1349-
Deno.test({
1350-
name: "[node/child_process spawn] supports numeric fd in stdio array",
1351-
// On Windows, Deno's fs.openSync returns a resource ID, not a CRT file
1352-
// descriptor, so libc::get_osfhandle receives an invalid fd and crashes.
1353-
ignore: Deno.build.os === "windows",
1354-
async fn() {
1355-
const tmpFile = await Deno.makeTempFile();
1356-
try {
1357-
const fd = fs.openSync(tmpFile, "r");
1358-
try {
1359-
// Passing a numeric fd at stdio index 3 should not throw.
1360-
// Previously this caused: serde_v8 error: invalid type; expected: enum, got: Number
1361-
const child = spawn(Deno.execPath(), [
1362-
"eval",
1363-
"/* no-op */",
1364-
], {
1365-
stdio: ["ignore", "ignore", "ignore", fd],
1366-
});
1367-
1368-
const deferred = withTimeout<number>();
1369-
child.on("exit", (code: number) => deferred.resolve(code));
1370-
const code = await deferred.promise;
1371-
assertEquals(code, 0);
1372-
} finally {
1373-
fs.closeSync(fd);
1374-
}
1375-
} finally {
1376-
await Deno.remove(tmpFile);
1377-
}
1378-
},
1379-
});

0 commit comments

Comments
 (0)