@@ -760,11 +760,36 @@ pub async fn op_node_rmdir(
760760 Ok ( ( ) )
761761}
762762
763+ /// Check if a handle is managed by a non-fsFile internal Deno resource.
764+ /// fsFile and signal resources are allowed to be dup'd (for node:fs and
765+ /// native addons like node-pty). Other managed resources (SQLite, internal
766+ /// pipes) are denied. Unmanaged handles are allowed through.
767+ fn deny_internal_resource_handle (
768+ state : & mut OpState ,
769+ handle : deno_core:: ResourceHandleFd ,
770+ ) -> Result < ( ) , FsError > {
771+ for ( rid, name) in state. resource_table . names ( ) {
772+ if name == "fsFile" || name == "signal" {
773+ continue ;
774+ }
775+ if matches ! (
776+ state. resource_table. get_handle( rid) ,
777+ Ok ( deno_core:: ResourceHandle :: Fd ( existing) ) if existing == handle
778+ ) {
779+ return Err ( FsError :: Io ( std:: io:: Error :: new (
780+ std:: io:: ErrorKind :: PermissionDenied ,
781+ "File descriptor is managed by an internal Deno resource" ,
782+ ) ) ) ;
783+ }
784+ }
785+ Ok ( ( ) )
786+ }
787+
763788/// Create a file resource from a raw file descriptor by dup'ing it first.
764789/// This is safe for cross-worker use because the dup'd fd is independently
765790/// owned and can be closed without affecting the original.
766791///
767- /// Safety: allows dup of fds that are either unmanaged (external: child_process
792+ /// Allows dup of fds that are either unmanaged (external: child_process
768793/// pipes, native addons) or owned by an "fsFile" resource (opened via node:fs).
769794/// Denies dup of fds managed by non-fsFile internal Deno resources (e.g.
770795/// SQLite, internal pipes) to prevent unauthorized access.
@@ -790,26 +815,7 @@ fn dup_fd_impl(state: &mut OpState, fd: i32) -> Result<ResourceId, FsError> {
790815 ) ) ) ;
791816 }
792817
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- }
818+ deny_internal_resource_handle ( state, fd) ?;
813819
814820 // SAFETY: dup() creates a new fd pointing to the same open file description.
815821 let new_fd = unsafe { libc:: dup ( fd) } ;
@@ -850,6 +856,48 @@ pub fn op_node_dup_fd(
850856 dup_fd_impl ( state, fd)
851857}
852858
859+ /// Like `libc::get_osfhandle` but safe to call with invalid fds.
860+ ///
861+ /// On Windows, CRT functions invoke the "invalid parameter handler" when
862+ /// given a bad fd, which by default terminates the process. We temporarily
863+ /// install a no-op handler so `get_osfhandle` returns -1 instead of
864+ /// aborting. This is the same pattern libuv uses (`uv__get_osfhandle`).
865+ ///
866+ /// Returns the raw HANDLE as `isize`, or -1 / -2 for invalid fds.
867+ #[ cfg( not( unix) ) ]
868+ fn safe_get_osfhandle ( fd : i32 ) -> isize {
869+ extern "C" fn silent_invalid_parameter_handler (
870+ _: * const u16 ,
871+ _: * const u16 ,
872+ _: * const u16 ,
873+ _: u32 ,
874+ _: usize ,
875+ ) {
876+ }
877+
878+ unsafe extern "C" {
879+ fn _set_thread_local_invalid_parameter_handler (
880+ handler : Option <
881+ unsafe extern "C" fn ( * const u16 , * const u16 , * const u16 , u32 , usize ) ,
882+ > ,
883+ ) -> Option <
884+ unsafe extern "C" fn ( * const u16 , * const u16 , * const u16 , u32 , usize ) ,
885+ > ;
886+ }
887+
888+ // SAFETY: _set_thread_local_invalid_parameter_handler and
889+ // get_osfhandle are standard CRT calls. The no-op handler is
890+ // thread-local and immediately restored after the call.
891+ unsafe {
892+ let prev = _set_thread_local_invalid_parameter_handler ( Some (
893+ silent_invalid_parameter_handler,
894+ ) ) ;
895+ let handle = libc:: get_osfhandle ( fd) ;
896+ _set_thread_local_invalid_parameter_handler ( prev) ;
897+ handle
898+ }
899+ }
900+
853901#[ cfg( not( unix) ) ]
854902fn dup_fd_impl ( state : & mut OpState , fd : i32 ) -> Result < ResourceId , FsError > {
855903 use std:: fs:: File as StdFile ;
@@ -863,64 +911,17 @@ fn dup_fd_impl(state: &mut OpState, fd: i32) -> Result<ResourceId, FsError> {
863911 ) ) ) ;
864912 }
865913
866- // Convert CRT fd to Windows HANDLE.
867- // get_osfhandle invokes the CRT invalid parameter handler for invalid
868- // fds, which by default terminates the process. We install a no-op
869- // handler (like libuv does) so it returns -1 instead.
870- // SAFETY: _set_thread_local_invalid_parameter_handler and
871- // get_osfhandle are safe CRT calls. The no-op handler prevents
872- // process termination for invalid fds.
873- let raw_handle = unsafe {
874- extern "C" fn silent_handler (
875- _: * const u16 ,
876- _: * const u16 ,
877- _: * const u16 ,
878- _: u32 ,
879- _: usize ,
880- ) {
881- }
882- unsafe extern "C" {
883- fn _set_thread_local_invalid_parameter_handler (
884- handler : Option <
885- unsafe extern "C" fn ( * const u16 , * const u16 , * const u16 , u32 , usize ) ,
886- > ,
887- ) -> Option <
888- unsafe extern "C" fn ( * const u16 , * const u16 , * const u16 , u32 , usize ) ,
889- > ;
890- }
891- let prev =
892- _set_thread_local_invalid_parameter_handler ( Some ( silent_handler) ) ;
893- let handle = libc:: get_osfhandle ( fd) ;
894- _set_thread_local_invalid_parameter_handler ( prev) ;
895- handle
896- } ;
914+ let raw_handle = safe_get_osfhandle ( fd) ;
897915 if raw_handle == -1 || raw_handle == -2 {
898916 // ERROR_INVALID_HANDLE (6) maps to EBADF via uvTranslateSysError
899917 return Err ( FsError :: Io ( std:: io:: Error :: from_raw_os_error ( 6 ) ) ) ;
900918 }
901919 let handle = raw_handle as RawHandle ;
902920
903- // Check if this handle is managed by a non-fsFile internal Deno resource.
904- // fsFile resources (opened via node:fs) are safe to dup.
905- // Other managed resources (SQLite, internal pipes) are denied.
906- // Unmanaged fds (child_process, native addons) are allowed through.
907- for ( rid, name) in state. resource_table . names ( ) {
908- if name == "fsFile" || name == "signal" {
909- continue ;
910- }
911- if matches ! (
912- state. resource_table. get_handle( rid) ,
913- Ok ( deno_core:: ResourceHandle :: Fd ( existing_handle) ) if existing_handle == handle
914- ) {
915- return Err ( FsError :: Io ( std:: io:: Error :: new (
916- std:: io:: ErrorKind :: PermissionDenied ,
917- "File descriptor is managed by an internal Deno resource" ,
918- ) ) ) ;
919- }
920- }
921+ deny_internal_resource_handle ( state, handle) ?;
921922
922923 // Duplicate the handle so it's independently owned and closeable.
923- // SAFETY: handle is valid (checked above via get_osfhandle ).
924+ // SAFETY: handle is valid (checked above via safe_get_osfhandle ).
924925 let borrowed = unsafe { BorrowedHandle :: borrow_raw ( handle) } ;
925926 let owned = borrowed. try_clone_to_owned ( ) . map_err ( FsError :: Io ) ?;
926927
0 commit comments