Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
94 changes: 61 additions & 33 deletions src/shims/unix/fd.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Self>,
Expand Down Expand Up @@ -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:
// <https://github.com/rust-lang/rust/commit/dda5c97675b4f5b1f6fdab64606c8a1f21021b0a>
// 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(
Expand Down Expand Up @@ -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") =>
Expand Down Expand Up @@ -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")
}
}
36 changes: 0 additions & 36 deletions src/shims/unix/tcp_socket.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::*;
Expand Down Expand Up @@ -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:
// <https://github.com/rust-lang/rust/commit/dda5c97675b4f5b1f6fdab64606c8a1f21021b0a>
// 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<Self>,
) -> Option<FileDescriptionRef<dyn UnixSocketFileDescription>> {
Expand Down
44 changes: 0 additions & 44 deletions src/shims/unix/virtual_socket.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::*;
Expand Down Expand Up @@ -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:
// <https://github.com/rust-lang/rust/commit/dda5c97675b4f5b1f6fdab64606c8a1f21021b0a>
// 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<Self>,
) -> Option<FileDescriptionRef<dyn UnixSocketFileDescription>> {
Expand Down
32 changes: 32 additions & 0 deletions tests/pass-dep/libc/pipe.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
Expand Down Expand Up @@ -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`.
Expand Down
106 changes: 106 additions & 0 deletions tests/pass-dep/libc/socket-no-blocking.rs
Original file line number Diff line number Diff line change
@@ -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);
}
Loading
Loading