@@ -1156,16 +1156,39 @@ pub const PosixSpawnResult = struct {
11561156 stdout : ? bun.FD = null ,
11571157 stderr : ? bun.FD = null ,
11581158 ipc : ? bun.FD = null ,
1159- extra_pipes : std .array_list .Managed (bun . FD ) = std .array_list .Managed (bun . FD ).init (bun .default_allocator ),
1159+ extra_pipes : std .array_list .Managed (ExtraPipe ) = std .array_list .Managed (ExtraPipe ).init (bun .default_allocator ),
11601160
11611161 memfds : [3 ]bool = .{ false , false , false },
11621162
11631163 // ESRCH can happen when requesting the pidfd
11641164 has_exited : bool = false ,
11651165
1166- pub fn close (this : * WindowsSpawnResult ) void {
1167- for (this .extra_pipes .items ) | fd | {
1168- fd .close ();
1166+ /// Entry in `extra_pipes` for a stdio slot at index >= 3.
1167+ pub const ExtraPipe = union (enum ) {
1168+ /// We created this fd (e.g. socketpair for `"pipe"`); expose it via
1169+ /// `Subprocess.stdio[N]` and close it in `finalizeStreams`.
1170+ owned_fd : bun.FD ,
1171+ /// The caller supplied this fd in the stdio array; expose it via
1172+ /// `Subprocess.stdio[N]` but never close it — the caller retains ownership.
1173+ unowned_fd : bun.FD ,
1174+ /// Nothing to expose for this slot (`"ignore"`, `"inherit"`, a path, or
1175+ /// the IPC channel after ownership has been transferred to uSockets).
1176+ unavailable : void ,
1177+ 1178+ pub fn fd (this : ExtraPipe ) bun.FD {
1179+ return switch (this ) {
1180+ .owned_fd , .unowned_fd = > | f | f ,
1181+ .unavailable = > bun .invalid_fd ,
1182+ };
1183+ }
1184+ };
1185+ 1186+ pub fn close (this : * PosixSpawnResult ) void {
1187+ for (this .extra_pipes .items ) | item | {
1188+ switch (item ) {
1189+ .owned_fd = > | f | f .close (),
1190+ .unowned_fd , .unavailable = > {},
1191+ }
11691192 }
11701193
11711194 this .extra_pipes .clearAndFree ();
@@ -1324,7 +1347,7 @@ pub fn spawnProcessPosix(
13241347 try actions .chdir (options .cwd );
13251348 }
13261349 var spawned = PosixSpawnResult {};
1327- var extra_fds = std .array_list .Managed (bun . FD ).init (bun .default_allocator );
1350+ var extra_fds = std .array_list .Managed (PosixSpawnResult . ExtraPipe ).init (bun .default_allocator );
13281351 errdefer extra_fds .deinit ();
13291352 var stack_fallback = std .heap .stackFallback (2048 , bun .default_allocator );
13301353 const allocator = stack_fallback .get ();
@@ -1480,16 +1503,16 @@ pub fn spawnProcessPosix(
14801503 .dup2 = > @panic ("TODO dup2 extra fd" ),
14811504 .inherit = > {
14821505 try actions .inherit (fileno );
1483- try extra_fds .append (bun . invalid_fd );
1506+ try extra_fds .append (.unavailable );
14841507 },
14851508 .ignore = > {
14861509 try actions .openZ (fileno , "/dev/null" , bun .O .RDWR , 0o664 );
1487- try extra_fds .append (bun . invalid_fd );
1510+ try extra_fds .append (.unavailable );
14881511 },
14891512
14901513 .path = > | path | {
14911514 try actions .open (fileno , path , bun .O .RDWR | bun .O .CREAT , 0o664 );
1492- try extra_fds .append (bun . invalid_fd );
1515+ try extra_fds .append (.unavailable );
14931516 },
14941517 .ipc , .buffer = > {
14951518 const fds : [2 ]bun.FD = try bun .sys .socketpair (
@@ -1508,14 +1531,14 @@ pub fn spawnProcessPosix(
15081531 try actions .dup2 (fds [1 ], fileno );
15091532 if (fds [1 ] != fileno )
15101533 try actions .close (fds [1 ]);
1511- try extra_fds .append (fds [0 ]);
1534+ try extra_fds .append (.{ . owned_fd = fds [0 ] } );
15121535 },
15131536 .pipe = > | fd | {
15141537 try actions .dup2 (fd , fileno );
15151538 // The fd was supplied by the caller (a number in the stdio array) and is
1516- // not owned by us. Record an invalid sentinel so finalizeStreams skips it
1517- // instead of closing the caller's descriptor out from under them .
1518- try extra_fds .append (bun . invalid_fd );
1539+ // not owned by us. Record it so `stdio[N]` returns the caller's fd, but
1540+ // mark it unowned so finalizeStreams leaves it open .
1541+ try extra_fds .append (.{ . unowned_fd = fd } );
15191542 },
15201543 }
15211544 }
@@ -1546,7 +1569,7 @@ pub fn spawnProcessPosix(
15461569 .result = > | pid | {
15471570 spawned .pid = pid ;
15481571 spawned .extra_pipes = extra_fds ;
1549- extra_fds = std .array_list .Managed (bun . FD ).init (bun .default_allocator );
1572+ extra_fds = std .array_list .Managed (PosixSpawnResult . ExtraPipe ).init (bun .default_allocator );
15501573
15511574 if (comptime Environment .isLinux ) {
15521575 // If it's spawnSync and we want to block the entire thread
0 commit comments