@@ -211,14 +211,50 @@ fn open_options_to_access_kind(open_options: &OpenOptions) -> OpenAccessKind {
211211 }
212212}
213213
214- #[ op2( fast, stack_trace) ]
215- #[ smi]
214+ #[ derive( Debug , Serialize ) ]
215+ pub struct OpenResult {
216+ pub rid : ResourceId ,
217+ pub fd : i32 ,
218+ }
219+
220+ /// Extract the real OS file descriptor from a Deno resource ID.
221+ #[ cfg( unix) ]
222+ fn get_fd_from_rid (
223+ state : & mut OpState ,
224+ rid : ResourceId ,
225+ ) -> Result < i32 , FsError > {
226+ let handle = state. resource_table . get_handle ( rid) . map_err ( |_| {
227+ FsError :: Io ( std:: io:: Error :: new (
228+ std:: io:: ErrorKind :: NotFound ,
229+ "Bad resource ID" ,
230+ ) )
231+ } ) ?;
232+ match handle {
233+ deno_core:: ResourceHandle :: Fd ( fd) => Ok ( fd) ,
234+ _ => Err ( FsError :: Io ( std:: io:: Error :: other (
235+ "Resource is not a file descriptor" ,
236+ ) ) ) ,
237+ }
238+ }
239+
240+ /// On Windows, RIDs are used directly as FDs (no real fd extraction).
241+ /// This preserves the existing behavior until Windows fd support is added.
242+ #[ cfg( not( unix) ) ]
243+ fn get_fd_from_rid (
244+ _state : & mut OpState ,
245+ rid : ResourceId ,
246+ ) -> Result < i32 , FsError > {
247+ Ok ( rid as i32 )
248+ }
249+
250+ #[ op2( stack_trace) ]
251+ #[ serde]
216252pub fn op_node_open_sync (
217253 state : & mut OpState ,
218254 #[ string] path : & str ,
219255 #[ smi] flags : i32 ,
220256 #[ smi] mode : u32 ,
221- ) -> Result < ResourceId , FsError > {
257+ ) -> Result < OpenResult , FsError > {
222258 let path = Path :: new ( path) ;
223259 let options = get_open_options ( flags, Some ( mode) ) ;
224260
@@ -232,17 +268,18 @@ pub fn op_node_open_sync(
232268 let rid = state
233269 . resource_table
234270 . add ( FileResource :: new ( file, "fsFile" . to_string ( ) ) ) ;
235- Ok ( rid)
271+ let fd = get_fd_from_rid ( state, rid) ?;
272+ Ok ( OpenResult { rid, fd } )
236273}
237274
238275#[ op2( stack_trace) ]
239- #[ smi ]
276+ #[ serde ]
240277pub async fn op_node_open (
241278 state : Rc < RefCell < OpState > > ,
242279 #[ string] path : String ,
243280 #[ smi] flags : i32 ,
244281 #[ smi] mode : u32 ,
245- ) -> Result < ResourceId , FsError > {
282+ ) -> Result < OpenResult , FsError > {
246283 let path = PathBuf :: from ( path) ;
247284 let options = get_open_options ( flags, Some ( mode) ) ;
248285
@@ -259,11 +296,12 @@ pub async fn op_node_open(
259296 } ;
260297 let file = fs. open_async ( path. as_owned ( ) , options) . await ?;
261298
299+ let mut state = state. borrow_mut ( ) ;
262300 let rid = state
263- . borrow_mut ( )
264301 . resource_table
265302 . add ( FileResource :: new ( file, "fsFile" . to_string ( ) ) ) ;
266- Ok ( rid)
303+ let fd = get_fd_from_rid ( & mut state, rid) ?;
304+ Ok ( OpenResult { rid, fd } )
267305}
268306#[ derive( Debug , Serialize ) ]
269307pub struct StatFs {
@@ -722,6 +760,99 @@ pub async fn op_node_rmdir(
722760 Ok ( ( ) )
723761}
724762
763+ /// Create a file resource from a raw file descriptor by dup'ing it first.
764+ /// This is safe for cross-worker use because the dup'd fd is independently
765+ /// owned and can be closed without affecting the original.
766+ ///
767+ /// Safety: allows dup of fds that are either unmanaged (external: child_process
768+ /// pipes, native addons) or owned by an "fsFile" resource (opened via node:fs).
769+ /// Denies dup of fds managed by non-fsFile internal Deno resources (e.g.
770+ /// SQLite, internal pipes) to prevent unauthorized access.
771+ #[ cfg( unix) ]
772+ #[ op2( fast) ]
773+ #[ smi]
774+ pub fn op_node_dup_fd (
775+ state : & mut OpState ,
776+ #[ smi] fd : i32 ,
777+ ) -> Result < ResourceId , FsError > {
778+ dup_fd_impl ( state, fd)
779+ }
780+
781+ #[ cfg( unix) ]
782+ fn dup_fd_impl ( state : & mut OpState , fd : i32 ) -> Result < ResourceId , FsError > {
783+ use std:: fs:: File as StdFile ;
784+ use std:: os:: unix:: io:: FromRawFd ;
785+
786+ if fd < 0 {
787+ return Err ( FsError :: Io ( std:: io:: Error :: new (
788+ std:: io:: ErrorKind :: InvalidInput ,
789+ "Invalid file descriptor" ,
790+ ) ) ) ;
791+ }
792+
793+ // Check if this fd is managed by a non-fsFile internal Deno resource.
794+ // fsFile resources (opened via node:fs) are safe to dup.
795+ // Other managed resources (SQLite, internal pipes) are denied.
796+ // Unmanaged fds (child_process, native addons) are allowed through.
797+ for ( rid, name) in state. resource_table . names ( ) {
798+ if name == "fsFile" || name == "signal" {
799+ // node:fs and node:signal resources are allowed to be dup'd for use in
800+ // native addons like node-pty
801+ continue ;
802+ }
803+ if matches ! (
804+ state. resource_table. get_handle( rid) ,
805+ Ok ( deno_core:: ResourceHandle :: Fd ( existing_fd) ) if existing_fd == fd
806+ ) {
807+ return Err ( FsError :: Io ( std:: io:: Error :: new (
808+ std:: io:: ErrorKind :: PermissionDenied ,
809+ "File descriptor is managed by an internal Deno resource" ,
810+ ) ) ) ;
811+ }
812+ }
813+
814+ // SAFETY: dup() creates a new fd pointing to the same open file description.
815+ let new_fd = unsafe { libc:: dup ( fd) } ;
816+ if new_fd < 0 {
817+ return Err ( FsError :: Io ( std:: io:: Error :: last_os_error ( ) ) ) ;
818+ }
819+
820+ // Clear O_NONBLOCK flag only for TTY fds (e.g. PTYs from node-pty).
821+ // StdFileResourceInner uses spawn_blocking for reads which expects
822+ // blocking I/O. Non-TTY fds keep their original flags.
823+ // SAFETY: new_fd is valid, isatty/fcntl are safe on valid fds
824+ unsafe {
825+ if libc:: isatty ( new_fd) == 1 {
826+ let flags = libc:: fcntl ( new_fd, libc:: F_GETFL ) ;
827+ if flags >= 0 && ( flags & libc:: O_NONBLOCK ) != 0 {
828+ libc:: fcntl ( new_fd, libc:: F_SETFL , flags & !libc:: O_NONBLOCK ) ;
829+ }
830+ }
831+ }
832+
833+ // SAFETY: new_fd is a valid fd we just created via dup().
834+ let std_file = unsafe { StdFile :: from_raw_fd ( new_fd) } ;
835+ let file: Rc < dyn deno_io:: fs:: File > =
836+ Rc :: new ( deno_io:: StdFileResourceInner :: file ( std_file, None ) ) ;
837+ let rid = state
838+ . resource_table
839+ . add ( FileResource :: new ( file, "fsFile" . to_string ( ) ) ) ;
840+ Ok ( rid)
841+ }
842+
843+ #[ cfg( not( unix) ) ]
844+ #[ op2( fast) ]
845+ #[ smi]
846+ pub fn op_node_dup_fd (
847+ _state : & mut OpState ,
848+ #[ smi] _fd : i32 ,
849+ ) -> Result < ResourceId , FsError > {
850+ Err ( FsError :: Io ( std:: io:: Error :: new (
851+ std:: io:: ErrorKind :: Unsupported ,
852+ "op_node_dup_fd is not supported on this platform" ,
853+ ) ) )
854+ }
855+
725856#[ derive( Debug , Serialize ) ]
726857#[ serde( rename_all = "camelCase" ) ]
727858pub struct CpStatInfo {
@@ -1585,4 +1716,128 @@ mod tests {
15851716 assert ! ( !is_src_subdir( & src, & sibling) ) ;
15861717 assert ! ( !is_src_subdir( & child, & src) ) ;
15871718 }
1719+
1720+ #[ cfg( unix) ]
1721+ #[ allow( clippy:: disallowed_methods) ]
1722+ mod dup_fd_tests {
1723+ use std:: borrow:: Cow ;
1724+ use std:: rc:: Rc ;
1725+
1726+ use deno_core:: OpState ;
1727+ use deno_core:: Resource ;
1728+ use deno_core:: ResourceHandle ;
1729+ use deno_core:: ResourceHandleFd ;
1730+ use deno_io:: fs:: FileResource ;
1731+
1732+ /// A mock non-fsFile resource that reports a specific fd via
1733+ /// `backing_handle`. Used to test that `op_node_dup_fd` denies
1734+ /// dup of fds managed by non-fsFile internal resources.
1735+ struct MockInternalResource {
1736+ fd : ResourceHandleFd ,
1737+ }
1738+
1739+ impl Resource for MockInternalResource {
1740+ fn name ( & self ) -> Cow < ' _ , str > {
1741+ // Intentionally NOT "fsFile" — simulates an internal resource
1742+ // like a SQLite handle or internal pipe.
1743+ "mockInternal" . into ( )
1744+ }
1745+
1746+ fn backing_handle ( self : Rc < Self > ) -> Option < ResourceHandle > {
1747+ Some ( ResourceHandle :: Fd ( self . fd ) )
1748+ }
1749+ }
1750+
1751+ #[ test]
1752+ fn dup_fd_denies_non_fsfile_resource ( ) {
1753+ // Create a real fd via a temp file so dup() would succeed
1754+ // if the security check weren't there.
1755+ let tmp = std:: env:: temp_dir ( ) . join ( "deno_dup_fd_test" ) ;
1756+ let file = std:: fs:: File :: create ( & tmp) . unwrap ( ) ;
1757+ use std:: os:: unix:: io:: AsRawFd ;
1758+ let fd = file. as_raw_fd ( ) ;
1759+
1760+ let mut state = OpState :: new ( None ) ;
1761+
1762+ // Register the fd under a non-fsFile resource name
1763+ state. resource_table . add ( MockInternalResource { fd } ) ;
1764+
1765+ // op_node_dup_fd should deny this fd
1766+ let result = super :: super :: dup_fd_impl ( & mut state, fd) ;
1767+ assert ! ( result. is_err( ) ) ;
1768+ match result. unwrap_err ( ) {
1769+ super :: super :: FsError :: Io ( e) => {
1770+ assert_eq ! ( e. kind( ) , std:: io:: ErrorKind :: PermissionDenied ) ;
1771+ }
1772+ other => {
1773+ panic ! ( "expected FsError::Io(PermissionDenied), got: {other:?}" )
1774+ }
1775+ }
1776+
1777+ drop ( file) ;
1778+ let _ = std:: fs:: remove_file ( & tmp) ;
1779+ }
1780+
1781+ #[ test]
1782+ fn dup_fd_allows_fsfile_resource ( ) {
1783+ // Create a real fd via a temp file
1784+ let tmp = std:: env:: temp_dir ( ) . join ( "deno_dup_fd_allow_test" ) ;
1785+ let std_file = std:: fs:: File :: create ( & tmp) . unwrap ( ) ;
1786+ use std:: os:: unix:: io:: AsRawFd ;
1787+ let fd = std_file. as_raw_fd ( ) ;
1788+
1789+ let mut state = OpState :: new ( None ) ;
1790+
1791+ // Register it as an "fsFile" resource (the allowed kind)
1792+ let file: Rc < dyn deno_io:: fs:: File > =
1793+ Rc :: new ( deno_io:: StdFileResourceInner :: file ( std_file, None ) ) ;
1794+ state
1795+ . resource_table
1796+ . add ( FileResource :: new ( file, "fsFile" . to_string ( ) ) ) ;
1797+
1798+ // op_node_dup_fd should allow this fd
1799+ let result = super :: super :: dup_fd_impl ( & mut state, fd) ;
1800+ assert ! ( result. is_ok( ) ) ;
1801+
1802+ // Clean up the dup'd resource
1803+ let dup_rid = result. unwrap ( ) ;
1804+ let _ = state. resource_table . take_any ( dup_rid) ;
1805+
1806+ let _ = std:: fs:: remove_file ( & tmp) ;
1807+ }
1808+
1809+ #[ test]
1810+ fn dup_fd_allows_unmanaged_fd ( ) {
1811+ // Create a real fd that is NOT in the resource table at all
1812+ let tmp = std:: env:: temp_dir ( ) . join ( "deno_dup_fd_unmanaged_test" ) ;
1813+ let file = std:: fs:: File :: create ( & tmp) . unwrap ( ) ;
1814+ use std:: os:: unix:: io:: AsRawFd ;
1815+ let fd = file. as_raw_fd ( ) ;
1816+
1817+ let mut state = OpState :: new ( None ) ;
1818+
1819+ // fd is valid but not in the resource table — should be allowed
1820+ let result = super :: super :: dup_fd_impl ( & mut state, fd) ;
1821+ assert ! ( result. is_ok( ) ) ;
1822+
1823+ let dup_rid = result. unwrap ( ) ;
1824+ let _ = state. resource_table . take_any ( dup_rid) ;
1825+
1826+ drop ( file) ;
1827+ let _ = std:: fs:: remove_file ( & tmp) ;
1828+ }
1829+
1830+ #[ test]
1831+ fn dup_fd_rejects_invalid_fd ( ) {
1832+ let mut state = OpState :: new ( None ) ;
1833+
1834+ // Negative fd should be rejected
1835+ let result = super :: super :: dup_fd_impl ( & mut state, -1 ) ;
1836+ assert ! ( result. is_err( ) ) ;
1837+
1838+ // Very high fd (likely invalid) should fail with EBADF
1839+ let result = super :: super :: dup_fd_impl ( & mut state, 99999 ) ;
1840+ assert ! ( result. is_err( ) ) ;
1841+ }
1842+ }
15881843}
0 commit comments