diff --git a/src/shims/unix/fd.rs b/src/shims/unix/fd.rs index 53342a3565..9bb6871856 100644 --- a/src/shims/unix/fd.rs +++ b/src/shims/unix/fd.rs @@ -77,20 +77,6 @@ pub trait UnixFileDescription: FileDescription { throw_unsup_format!("fcntl: {} is not supported for F_SETFL", self.name()); } - /// Modifies device parameters. - /// `op` is the device-dependent operation code. It's either a `c_long` or `c_int`, depending on - /// the target and whether it uses glibc or musl. - /// `arg` is the optional third argument which exists depending on the operation code. It's either - /// an integer or a pointer. - fn ioctl<'tcx>( - &self, - _op: Scalar, - _args: Varargs<'tcx, '_>, - _ecx: &mut MiriInterpCx<'tcx>, - ) -> InterpResult<'tcx, i32> { - throw_unsup_format!("cannot use ioctl on {}", self.name()); - } - /// Returns this file description as a Unix socket, if it represents one. fn as_socket( self: FileDescriptionRef, @@ -222,15 +208,52 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> { // Handle common opcodes. let fioclex = this.eval_libc("FIOCLEX"); let fionclex = this.eval_libc("FIONCLEX"); + let fionbio = this.eval_libc("FIONBIO"); + if op == fioclex || op == fionclex { // Since we don't support `exec`, those are NOPs. - return interp_ok(Scalar::from_i32(0)); - } + interp_ok(Scalar::from_i32(0)) + } else if op == fionbio { + // On these OSes, the Rust standard library uses the ioctl for changing the blocking mode + // of certain file descriptors, so we trust that it is reasonable and controls the same + // internal flag as fcntl. + if !matches!(this.tcx.sess.target.os, Os::Linux | Os::Android | Os::MacOs | Os::FreeBsd) + { + // FIONBIO cannot be used to change the blocking mode of some file descriptors on solarish targets: + // + // Since there might be more targets which do weird things with this option, we use + // an allowlist instead of just denying solarish targets. + throw_unsup_format!( + "ioctl: setting FIONBIO is unsupported on {} on target {}", + fd.name(), + this.tcx.sess.target.os + ); + } - // Since some ioctl operations use the return value as an output parameter, we cannot strictly use the convention of - // zero indicating success and -1 indicating an error. - let return_value = fd.as_unix().ioctl(op, varargs, this)?; - interp_ok(Scalar::from_i32(return_value)) + let ([value_ptr], _) = this.check_varargs(shim_varargs![*i32], varargs, "ioctl")?; + let value = this.deref_pointer(&value_ptr)?; + let non_block = this.read_scalar(&value)?.to_i32()? != 0; + + let o_nonblock = this.eval_libc_i32("O_NONBLOCK"); + + // We change the blocking mode of the file description using `set_flags`. + // For this we first fetch the current flags using `get_flags` and then + // we add or subtract O_NONBLOCK to them. + + let unix_fd = fd.as_unix(); + let mut flags = unix_fd.get_flags(this)?.to_i32()?; + if non_block { + // Add O_NONBLOCK to the existing flags. + flags |= o_nonblock + } else { + // Remove O_NONBLOCK from the existing flags. + flags &= !o_nonblock + } + unix_fd.set_flags(flags & !this.setfl_ignored_flags(), this)?; + interp_ok(Scalar::from_i32(0)) + } else { + throw_unsup_format!("ioctl: unsupported operation {op:#x} on {}", fd.name()) + } } fn fcntl( @@ -306,19 +329,7 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> { this.check_varargs(shim_varargs![i32], varargs, "fcntl(fd, F_SETFL, ...)")?; let flag = this.read_scalar(&flag)?.to_i32()?; - // Ignore flags that never get stored by SETFL. - // "File access mode (O_RDONLY, O_WRONLY, O_RDWR) and file - // creation flags (i.e., O_CREAT, O_EXCL, O_NOCTTY, O_TRUNC) - // in arg are ignored." - let ignored_flags = this.eval_libc_i32("O_RDONLY") - | this.eval_libc_i32("O_WRONLY") - | this.eval_libc_i32("O_RDWR") - | this.eval_libc_i32("O_CREAT") - | this.eval_libc_i32("O_EXCL") - | this.eval_libc_i32("O_NOCTTY") - | this.eval_libc_i32("O_TRUNC"); - - fd.as_unix().set_flags(flag & !ignored_flags, this) + fd.as_unix().set_flags(flag & !this.setfl_ignored_flags(), this) } cmd if this.tcx.sess.target.os == Os::MacOs && cmd == this.eval_libc_i32("F_FULLFSYNC") => @@ -805,4 +816,21 @@ trait EvalContextPrivExt<'tcx>: crate::MiriInterpCxExt<'tcx> { }; interp_ok(()) } + + /// Flags that never get stored by SETFL. + fn setfl_ignored_flags(&self) -> i32 { + let this = self.eval_context_ref(); + + // The F_SETFL man page specifies that the "[...] File access mode + // (O_RDONLY, O_WRONLY, O_RDWR) and file creation flags (i.e., O_CREAT, + // O_EXCL, O_NOCTTY, O_TRUNC) in arg are ignored." + + this.eval_libc_i32("O_RDONLY") + | this.eval_libc_i32("O_WRONLY") + | this.eval_libc_i32("O_RDWR") + | this.eval_libc_i32("O_CREAT") + | this.eval_libc_i32("O_EXCL") + | this.eval_libc_i32("O_NOCTTY") + | this.eval_libc_i32("O_TRUNC") + } } diff --git a/src/shims/unix/tcp_socket.rs b/src/shims/unix/tcp_socket.rs index 485ab87d8b..07f7ac7d7f 100644 --- a/src/shims/unix/tcp_socket.rs +++ b/src/shims/unix/tcp_socket.rs @@ -12,7 +12,6 @@ use rustc_middle::throw_unsup_format; use rustc_target::spec::Os; use crate::shims::files::{EvalContextExt as _, FdNum, FileDescription, FileDescriptionRef}; -use crate::shims::sig::Varargs; use crate::shims::unix::UnixFileDescription; use crate::shims::unix::socket::{EvalContextExt, UnixSocketFileDescription}; use crate::*; @@ -241,41 +240,6 @@ impl UnixFileDescription for TcpSocket { interp_ok(Scalar::from_i32(0)) } - fn ioctl<'tcx>( - &self, - op: Scalar, - args: Varargs<'tcx, '_>, - ecx: &mut MiriInterpCx<'tcx>, - ) -> InterpResult<'tcx, i32> { - assert!(ecx.machine.communicate(), "cannot have `TcpSocket` with isolation enabled!"); - - let fionbio = ecx.eval_libc("FIONBIO"); - - if op == fionbio { - // On these OSes, Rust uses the ioctl, so we trust that it is reasonable and controls - // the same internal flag as fcntl. - if !matches!(ecx.tcx.sess.target.os, Os::Linux | Os::Android | Os::MacOs | Os::FreeBsd) - { - // FIONBIO cannot be used to change the blocking mode of a socket on solarish targets: - // - // Since there might be more targets which do weird things with this option, we use - // an allowlist instead of just denying solarish targets. - throw_unsup_format!( - "ioctl: setting FIONBIO on sockets is unsupported on target {}", - ecx.tcx.sess.target.os - ); - } - - let ([value_ptr], _) = ecx.check_varargs(shim_varargs![*i32], args, "ioctl")?; - let value = ecx.deref_pointer(&value_ptr)?; - let non_block = ecx.read_scalar(&value)?.to_i32()? != 0; - self.is_non_block.set(non_block); - return interp_ok(0); - } - - throw_unsup_format!("ioctl: unsupported operation {op:#x} on socket"); - } - fn as_socket( self: FileDescriptionRef, ) -> Option> { diff --git a/src/shims/unix/virtual_socket.rs b/src/shims/unix/virtual_socket.rs index cf2ed1f5d2..e712bb35aa 100644 --- a/src/shims/unix/virtual_socket.rs +++ b/src/shims/unix/virtual_socket.rs @@ -15,7 +15,6 @@ use crate::shims::files::{ EvalContextExt as _, FileDescription, FileDescriptionRef, WeakFileDescriptionRef, }; use crate::shims::readiness::DelayedReadinessUpdates; -use crate::shims::sig::Varargs; use crate::shims::unix::UnixFileDescription; use crate::shims::unix::socket::UnixSocketFileDescription; use crate::*; @@ -257,49 +256,6 @@ impl UnixFileDescription for VirtualSocket { interp_ok(Scalar::from_i32(0)) } - fn ioctl<'tcx>( - &self, - op: Scalar, - args: Varargs<'tcx, '_>, - ecx: &mut MiriInterpCx<'tcx>, - ) -> InterpResult<'tcx, i32> { - match self.fd_type { - VirtualSocketType::Socketpair => { /* fall-through to below */ } - VirtualSocketType::PipeRead | VirtualSocketType::PipeWrite => { - // The standard library only uses ioctl for changing the blocking mode - // of Unix sockets. Thus, since using ioctl isn't the preferred way of - // changing the blocking mode, we don't support it on pipes. - throw_unsup_format!("cannot use ioctl on pipe"); - } - } - - let fionbio = ecx.eval_libc("FIONBIO"); - - if op == fionbio { - // On these OSes, Rust uses the ioctl, so we trust that it is reasonable and controls - // the same internal flag as fcntl. - if !matches!(ecx.tcx.sess.target.os, Os::Linux | Os::Android | Os::MacOs | Os::FreeBsd) - { - // FIONBIO cannot be used to change the blocking mode of a socket on solarish targets: - // - // Since there might be more targets which do weird things with this option, we use - // an allowlist instead of just denying solarish targets. - throw_unsup_format!( - "ioctl: setting FIONBIO on sockets is unsupported on target {}", - ecx.tcx.sess.target.os - ); - } - - let ([value_ptr], _) = ecx.check_varargs(shim_varargs![*i32], args, "ioctl")?; - let value = ecx.deref_pointer(&value_ptr)?; - let non_block = ecx.read_scalar(&value)?.to_i32()? != 0; - self.is_nonblock.set(non_block); - return interp_ok(0); - } - - throw_unsup_format!("ioctl: unsupported operation {op:#x} on socket"); - } - fn as_socket( self: FileDescriptionRef, ) -> Option> { diff --git a/tests/pass-dep/libc/pipe.rs b/tests/pass-dep/libc/pipe.rs index a4ca8ac01d..a179ec1d61 100644 --- a/tests/pass-dep/libc/pipe.rs +++ b/tests/pass-dep/libc/pipe.rs @@ -33,6 +33,8 @@ fn main() { // `pipe2` only exists in some specific os. test_pipe2(); test_pipe_setfl_getfl(); + #[cfg(not(any(target_os = "solaris", target_os = "illumos")))] + test_pipe_ioctl_fionbio(); test_pipe_fcntl_threaded(); test_send_recv(); } @@ -187,6 +189,36 @@ fn test_pipe_setfl_getfl() { ); } +#[cfg(not(any(target_os = "solaris", target_os = "illumos")))] +/// Test changing the blocking state of one end of a pipe using the `ioctl(fd, FIONBIO, ...)` syscall. +fn test_pipe_ioctl_fionbio() { + let mut fds = [-1, -1]; + errno_check(unsafe { libc::pipe(fds.as_mut_ptr()) }); + + unsafe { + // Change socket to be non-blocking. + let mut value = 1 as libc::c_int; + errno_check(libc::ioctl(fds[0], libc::FIONBIO, &mut value)); + } + + let flags_read_end = unsafe { errno_result(libc::fcntl(fds[0], libc::F_GETFL, 0)).unwrap() }; + let flags_write_end = unsafe { errno_result(libc::fcntl(fds[1], libc::F_GETFL, 0)).unwrap() }; + // Ensure that the read end is really non-blocking. + assert_eq!(flags_read_end & libc::O_NONBLOCK, libc::O_NONBLOCK); + // Ensure that the write end is still blocking. + assert_eq!(flags_write_end & libc::O_NONBLOCK, 0); + + unsafe { + // Change read end back to be blocking. + let mut value = 0 as libc::c_int; + errno_check(libc::ioctl(fds[0], libc::FIONBIO, &mut value)); + } + + let flags = unsafe { errno_result(libc::fcntl(fds[0], libc::F_GETFL, 0)).unwrap() }; + // Ensure that the read end is really blocking. + assert_eq!(flags & libc::O_NONBLOCK, 0); +} + /// Test the behaviour of F_SETFL/F_GETFL when a fd is blocking. /// The expected execution is: /// 1. Main thread blocks on fds[0] `read`. diff --git a/tests/pass-dep/libc/socket-no-blocking.rs b/tests/pass-dep/libc/socket-no-blocking.rs new file mode 100644 index 0000000000..19a71c59fd --- /dev/null +++ b/tests/pass-dep/libc/socket-no-blocking.rs @@ -0,0 +1,106 @@ +//@ignore-target: windows # No libc socket on Windows +//@compile-flags: -Zmiri-disable-isolation +//@revisions: tcp udp +//@run-native + +#[path = "../../utils/libc.rs"] +mod libc_utils; +#[path = "../../utils/mod.rs"] +mod utils; + +use libc_utils::*; + +// This file contains tests which are executed with different socket types (e.g., TCP, UDP, ...). +// Every socket type has its own revision and based on that the `SOCKET_TYPE` constant gets set. + +cfg_select! { + tcp => { + const SOCKET_TYPE: libc::c_int = libc::SOCK_STREAM; + } + udp => { + const SOCKET_TYPE: libc::c_int = libc::SOCK_DGRAM; + } +} + +fn main() { + test_fcntl_nonblock_opt(); + #[cfg(any( + target_os = "linux", + target_os = "android", + target_os = "freebsd", + target_os = "solaris", + target_os = "illumos" + ))] + test_sock_nonblock_opt(); + #[cfg(not(any(target_os = "solaris", target_os = "illumos")))] + test_ioctl_fionbio_op(); +} + +/// Test that setting the O_NONBLOCK flag changes the blocking state of a socket. +fn test_fcntl_nonblock_opt() { + let sockfd = unsafe { errno_result(libc::socket(libc::AF_INET, SOCKET_TYPE, 0)).unwrap() }; + + unsafe { + // Change socket to be non-blocking. + errno_check(libc::fcntl(sockfd, libc::F_SETFL, libc::O_NONBLOCK)); + } + + let flags = unsafe { errno_result(libc::fcntl(sockfd, libc::F_GETFL, 0)).unwrap() }; + // Ensure that socket is really non-blocking. + assert_eq!(flags & libc::O_NONBLOCK, libc::O_NONBLOCK); + + unsafe { + // Change socket back to be blocking. + errno_check(libc::fcntl(sockfd, libc::F_SETFL, 0)); + } + + let flags = unsafe { errno_result(libc::fcntl(sockfd, libc::F_GETFL, 0)).unwrap() }; + // Ensure that socket is really blocking. + assert_eq!(flags & libc::O_NONBLOCK, 0); +} + +#[cfg(any( + target_os = "linux", + target_os = "android", + target_os = "freebsd", + target_os = "solaris", + target_os = "illumos" +))] +/// Test creating a non-blocking socket by using the SOCK_NONBLOCK option +/// for the `socket` syscall. +fn test_sock_nonblock_opt() { + let sockfd = unsafe { + errno_result(libc::socket(libc::AF_INET, SOCKET_TYPE | libc::SOCK_NONBLOCK, 0)).unwrap() + }; + + let flags = unsafe { errno_result(libc::fcntl(sockfd, libc::F_GETFL, 0)).unwrap() }; + // Ensure that socket is really non-blocking. + assert_eq!(flags & libc::O_NONBLOCK, libc::O_NONBLOCK); +} + +#[cfg(not(any(target_os = "solaris", target_os = "illumos")))] +/// Test changing the blocking state of a socket using the `ioctl(fd, FIONBIO, ...)` +/// syscall. +fn test_ioctl_fionbio_op() { + let sockfd = unsafe { errno_result(libc::socket(libc::AF_INET, SOCKET_TYPE, 0)).unwrap() }; + + unsafe { + // Change socket to be non-blocking. + let mut value = 1 as libc::c_int; + errno_check(libc::ioctl(sockfd, libc::FIONBIO, &mut value)); + } + + let flags = unsafe { errno_result(libc::fcntl(sockfd, libc::F_GETFL, 0)).unwrap() }; + // Ensure that socket is really non-blocking. + assert_eq!(flags & libc::O_NONBLOCK, libc::O_NONBLOCK); + + unsafe { + // Change socket back to be blocking. + let mut value = 0 as libc::c_int; + errno_check(libc::ioctl(sockfd, libc::FIONBIO, &mut value)); + } + + let flags = unsafe { errno_result(libc::fcntl(sockfd, libc::F_GETFL, 0)).unwrap() }; + // Ensure that socket is really blocking. + assert_eq!(flags & libc::O_NONBLOCK, 0); +} diff --git a/tests/pass-dep/libc/socket-tcp-no-blocking.rs b/tests/pass-dep/libc/socket-tcp-no-blocking.rs index cb2f6af561..6b1d15333e 100644 --- a/tests/pass-dep/libc/socket-tcp-no-blocking.rs +++ b/tests/pass-dep/libc/socket-tcp-no-blocking.rs @@ -30,18 +30,6 @@ fn yield_now() { } fn main() { - test_fcntl_nonblock_opt(); - #[cfg(any( - target_os = "linux", - target_os = "android", - target_os = "freebsd", - target_os = "solaris", - target_os = "illumos" - ))] - test_sock_nonblock_opt(); - #[cfg(not(any(target_os = "solaris", target_os = "illumos")))] - test_ioctl_fionbio_op(); - test_accept_nonblock(); #[cfg(any( target_os = "linux", @@ -74,78 +62,6 @@ fn main() { test_connect_connecting(); } -/// Test that setting the O_NONBLOCK flag changes the blocking state of a socket. -fn test_fcntl_nonblock_opt() { - let sockfd = - unsafe { errno_result(libc::socket(libc::AF_INET, libc::SOCK_STREAM, 0)).unwrap() }; - - unsafe { - // Change socket to be non-blocking. - errno_check(libc::fcntl(sockfd, libc::F_SETFL, libc::O_NONBLOCK)); - } - - let flags = unsafe { errno_result(libc::fcntl(sockfd, libc::F_GETFL, 0)).unwrap() }; - // Ensure that socket is really non-blocking. - assert_eq!(flags & libc::O_NONBLOCK, libc::O_NONBLOCK); - - unsafe { - // Change socket back to be blocking. - errno_check(libc::fcntl(sockfd, libc::F_SETFL, 0)); - } - - let flags = unsafe { errno_result(libc::fcntl(sockfd, libc::F_GETFL, 0)).unwrap() }; - // Ensure that socket is really blocking. - assert_eq!(flags & libc::O_NONBLOCK, 0); -} - -#[cfg(any( - target_os = "linux", - target_os = "android", - target_os = "freebsd", - target_os = "solaris", - target_os = "illumos" -))] -/// Test creating a non-blocking socket by using the SOCK_NONBLOCK option -/// for the `socket` syscall. -fn test_sock_nonblock_opt() { - let sockfd = unsafe { - errno_result(libc::socket(libc::AF_INET, libc::SOCK_STREAM | libc::SOCK_NONBLOCK, 0)) - .unwrap() - }; - - let flags = unsafe { errno_result(libc::fcntl(sockfd, libc::F_GETFL, 0)).unwrap() }; - // Ensure that socket is really non-blocking. - assert_eq!(flags & libc::O_NONBLOCK, libc::O_NONBLOCK); -} - -#[cfg(not(any(target_os = "solaris", target_os = "illumos")))] -/// Test changing the blocking state of a socket using the `ioctl(fd, FIONBIO, ...)` -/// syscall. -fn test_ioctl_fionbio_op() { - let sockfd = - unsafe { errno_result(libc::socket(libc::AF_INET, libc::SOCK_STREAM, 0)).unwrap() }; - - unsafe { - // Change socket to be non-blocking. - let mut value = 1 as libc::c_int; - errno_check(libc::ioctl(sockfd, libc::FIONBIO, &mut value)); - } - - let flags = unsafe { errno_result(libc::fcntl(sockfd, libc::F_GETFL, 0)).unwrap() }; - // Ensure that socket is really non-blocking. - assert_eq!(flags & libc::O_NONBLOCK, libc::O_NONBLOCK); - - unsafe { - // Change socket back to be blocking. - let mut value = 0 as libc::c_int; - errno_check(libc::ioctl(sockfd, libc::FIONBIO, &mut value)); - } - - let flags = unsafe { errno_result(libc::fcntl(sockfd, libc::F_GETFL, 0)).unwrap() }; - // Ensure that socket is really blocking. - assert_eq!(flags & libc::O_NONBLOCK, 0); -} - /// Test that nonblocking TCP server sockets return [`ErrorKind::WouldBlock`] when trying /// to accept when no incoming connection exists. This also tests that nonblocking server sockets /// are still able to accept incoming connections should they already exist before the `accept` or