From a076bf50e3b4853d653d80184f56e2eca0ce60a0 Mon Sep 17 00:00:00 2001 From: WhySoBad <49595640+WhySoBad@users.noreply.github.com> Date: Mon, 19 Oct 2026 23:57:43 +0200 Subject: [PATCH 1/3] Add `sendto` support to `UnixSocketFileDescription` --- src/shims/unix/foreign_items.rs | 9 +- src/shims/unix/socket.rs | 26 ++++++ src/shims/unix/solarish/foreign_items.rs | 7 ++ src/shims/unix/tcp_socket.rs | 13 ++- src/shims/unix/virtual_socket.rs | 4 + tests/pass-dep/libc/socket-tcp.rs | 101 +++++++++++++++++++++++ 6 files changed, 158 insertions(+), 2 deletions(-) diff --git a/src/shims/unix/foreign_items.rs b/src/shims/unix/foreign_items.rs index cae8e314db..c3af6bfb46 100644 --- a/src/shims/unix/foreign_items.rs +++ b/src/shims/unix/foreign_items.rs @@ -684,7 +684,14 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> { shim_sig!(extern "C" fn(i32, *_, libc::size_t, i32) -> libc::ssize_t), (link_name, abi, args), )?; - this.send(socket, buffer, length, flags, dest)?; + this.send(socket, buffer, length, flags, /* dest_addr */ None, dest)?; + } + "sendto" => { + let [socket, buffer, length, flags, dest_addr, dest_addr_len] = &this.check_shim_sig( + shim_sig!(extern "C" fn(i32, *_, libc::size_t, i32, *libc::sockaddr, libc::socklen_t) -> libc::ssize_t), + (link_name, abi, args), + )?; + this.send(socket, buffer, length, flags, Some((dest_addr, dest_addr_len)), dest)?; } "recv" => { let [socket, buffer, length, flags] = &this.check_shim_sig( diff --git a/src/shims/unix/socket.rs b/src/shims/unix/socket.rs index 7ce0b0613d..2caa8c0d70 100644 --- a/src/shims/unix/socket.rs +++ b/src/shims/unix/socket.rs @@ -87,6 +87,8 @@ pub trait UnixSocketFileDescription: UnixFileDescription { /// Send data from the given buffer `ptr` into the socket. /// `len` indicates how many bytes we should try to send. /// `is_non_block` specifies whether the send operation is non-blocking. + /// `dest_addr` optionally contains a socket address to which the data + /// should be sent instead of the default destination address. /// After a successful send, `finish` should be called with the amount of bytes sent. fn send<'tcx>( self: FileDescriptionRef, @@ -94,6 +96,7 @@ pub trait UnixSocketFileDescription: UnixFileDescription { _ptr: Pointer, _len: usize, _is_non_block: bool, + _dest_addr: Option, _ecx: &mut MiriInterpCx<'tcx>, _finish: DynMachineCallback<'tcx, Result>, ) -> InterpResult<'tcx> { @@ -492,6 +495,7 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> { buffer: &OpTy<'tcx>, length: &OpTy<'tcx>, flags: &OpTy<'tcx>, + dest_addr: Option<(&OpTy<'tcx>, &OpTy<'tcx>)>, // Location where the output scalar is written to. dest: &MPlaceTy<'tcx>, ) -> InterpResult<'tcx> { @@ -504,6 +508,27 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> { this.read_scalar(length)?.to_uint(size_layout.size)?.try_into().unwrap(); let mut flags = this.read_scalar(flags)?.to_i32()?; + let dest_addr = match dest_addr { + Some((dest_addr, dest_addr_len)) => { + let dest_addr_ptr = this.read_pointer(dest_addr)?; + let socklen_layout = this.libc_ty_layout("socklen_t"); + let dest_addr_len_int = + this.read_scalar(dest_addr_len)?.to_uint(socklen_layout.size)?; + + if dest_addr_ptr == Pointer::null() || dest_addr_len_int == 0 { + // If the address pointer is a nullptr or the address length is zero + // we ignore the destination address. + None + } else { + match this.read_socket_address(dest_addr, dest_addr_len, "sendto")? { + Ok(dest_addr) => Some(dest_addr), + Err(e) => return this.set_errno_and_return_neg1(e, dest), + } + } + } + None => None, + }; + // Get the file handle let Some(fd) = this.machine.fds.get(socket) else { return this.set_errno_and_return_neg1(LibcError("EBADF"), dest); @@ -550,6 +575,7 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> { buffer_ptr, length, is_non_block, + dest_addr, this, callback!( @capture<'tcx> { diff --git a/src/shims/unix/solarish/foreign_items.rs b/src/shims/unix/solarish/foreign_items.rs index 055d710f2e..3628ec259a 100644 --- a/src/shims/unix/solarish/foreign_items.rs +++ b/src/shims/unix/solarish/foreign_items.rs @@ -132,6 +132,13 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> { )?; this.connect(socket, address, address_len, dest)?; } + "__xnet_sendto" => { + let [socket, buffer, length, flags, dest_addr, dest_addr_len] = &this.check_shim_sig( + shim_sig!(extern "C" fn(i32, *_, libc::size_t, i32, *libc::sockaddr, libc::socklen_t) -> libc::ssize_t), + (link_name, abi, args), + )?; + this.send(socket, buffer, length, flags, Some((dest_addr, dest_addr_len)), dest)?; + } "__xnet_getaddrinfo" => { let [node, service, hints, res] = &this.check_shim_sig( shim_sig!(extern "C" fn(*_, *_, *_, **_) -> i32), diff --git a/src/shims/unix/tcp_socket.rs b/src/shims/unix/tcp_socket.rs index 7e24256b6c..3ebb5a92f4 100644 --- a/src/shims/unix/tcp_socket.rs +++ b/src/shims/unix/tcp_socket.rs @@ -182,7 +182,15 @@ impl FileDescription for TcpSocket { ecx: &mut MiriInterpCx<'tcx>, finish: DynMachineCallback<'tcx, Result>, ) -> InterpResult<'tcx> { - self.send(communicate_allowed, ptr, len, /* is_non_block */ false, ecx, finish) + self.send( + communicate_allowed, + ptr, + len, + /* is_non_block */ false, + /* dest_addr */ None, + ecx, + finish, + ) } fn short_fd_operations(&self) -> bool { @@ -585,6 +593,9 @@ impl UnixSocketFileDescription for TcpSocket { ptr: Pointer, len: usize, is_non_block: bool, + // For stream-based sockets `send` and `sendto` are equivalent and + // we can thus ignore the destination address. + _dest_addr: Option, ecx: &mut MiriInterpCx<'tcx>, finish: DynMachineCallback<'tcx, Result>, ) -> InterpResult<'tcx> { diff --git a/src/shims/unix/virtual_socket.rs b/src/shims/unix/virtual_socket.rs index 06c05b5e9f..a192321c92 100644 --- a/src/shims/unix/virtual_socket.rs +++ b/src/shims/unix/virtual_socket.rs @@ -5,6 +5,7 @@ use std::cell::{Cell, OnceCell, RefCell}; use std::collections::VecDeque; use std::io::{self, ErrorKind, Read}; +use std::net::SocketAddr; use std::rc::Rc; use rustc_target::spec::Os; @@ -273,6 +274,9 @@ impl UnixSocketFileDescription for VirtualSocket { ptr: Pointer, len: usize, is_non_block: bool, + // For stream-based sockets `send` and `sendto` are equivalent and + // we can thus ignore the destination address. + _dest_addr: Option, ecx: &mut MiriInterpCx<'tcx>, finish: DynMachineCallback<'tcx, Result>, ) -> InterpResult<'tcx> { diff --git a/tests/pass-dep/libc/socket-tcp.rs b/tests/pass-dep/libc/socket-tcp.rs index 6ed06314e7..5f1fcfb9ce 100644 --- a/tests/pass-dep/libc/socket-tcp.rs +++ b/tests/pass-dep/libc/socket-tcp.rs @@ -40,6 +40,7 @@ fn main() { test_connect_listening(); test_send_peek_recv(); test_write_read(); + test_sendto(); test_readv(); test_writev(); @@ -398,6 +399,106 @@ fn test_write_read() { server_thread.join().unwrap(); } +/// Test sending data into a TCP socket using `sendto`. +/// We provide a nullptr as destination address, which should not be +/// a problem since TCP is a connection-mode protocol and thus the +/// destination address should be ignored. +fn test_sendto() { + let (server_sockfd, addr) = net::make_listener_ipv4().unwrap(); + let client_sockfd = + unsafe { errno_result(libc::socket(libc::AF_INET, libc::SOCK_STREAM, 0)).unwrap() }; + + // Spawn the server thread. + let server_thread = thread::spawn(move || { + let (peerfd, _) = net::accept_ipv4(server_sockfd).unwrap(); + + // Write some bytes into the stream using `sendto`. + + if cfg!(miri) || !cfg!(any(target_os = "freebsd", target_vendor = "apple")) { + // Native FreeBSD and macOS return EINVAL when the address pointer is invalid even when + // the address length is zero. However, POSIX says the address pointer should be entirely + // ignored for streaming sockets. We thus skip those native tests. + + // When the length is zero, the invalid address pointer is ignored. We thus provide + // a garbage pointer to ensure this behavior. + unsafe { + libc_utils::write_all_generic( + TEST_BYTES.as_ptr().cast(), + TEST_BYTES.len(), + libc_utils::NoRetry, + |buf, count| { + libc::sendto( + peerfd, + buf, + count, + /* flags */ 0, + ptr::without_provenance_mut::(16), + /* address_len */ 0, + ) + }, + ) + .unwrap(); + } + } + + // When the address pointer and the length are valid, the destination + // address should be ignored since we're using TCP. + let valid_addr = net::sock_addr_ipv4([127, 0, 0, 1], 5555); + unsafe { + libc_utils::write_all_generic( + TEST_BYTES.as_ptr().cast(), + TEST_BYTES.len(), + libc_utils::NoRetry, + |buf, count| { + libc::sendto( + peerfd, + buf, + count, + /* flags */ 0, + (&valid_addr as *const libc::sockaddr_in).cast::(), + size_of::() as libc::socklen_t, + ) + }, + ) + .unwrap(); + } + + if cfg!(miri) || !cfg!(any(target_os = "freebsd", target_vendor = "apple")) { + // On macOS and FreeBSD hosts we get EINVAL when the address pointer is a nullptr. + // This is not specified in their man pages and differs from the behavior of most + // other Unixes. We thus skip this `sendto` on those hosts. + + // When the address pointer is a nullptr it should be ignored. + unsafe { + libc_utils::write_all_generic( + TEST_BYTES.as_ptr().cast(), + TEST_BYTES.len(), + libc_utils::NoRetry, + |buf, count| { + libc::sendto( + peerfd, + buf, + count, + /* flags */ 0, + ptr::null_mut::(), + /* address_len */ 16, + ) + }, + ) + .unwrap(); + } + } + }); + + net::connect_ipv4(client_sockfd, addr).unwrap(); + + let mut buffer = [0; TEST_BYTES.len()]; + libc_utils::read_exact(client_sockfd, &mut buffer).unwrap(); + assert_eq!(&buffer, TEST_BYTES); + + server_thread.join().unwrap(); +} + /// Test vectored reads with multiple buffers on a connected socket. fn test_readv() { let (server_sockfd, addr) = net::make_listener_ipv4().unwrap(); From f272d5eb42f732d960d5e02d39925c87790b944e Mon Sep 17 00:00:00 2001 From: WhySoBad <49595640+WhySoBad@users.noreply.github.com> Date: Tue, 20 Oct 2026 00:22:20 +0200 Subject: [PATCH 2/3] Add `recvfrom` support to `UnixSocketFileDescription` --- src/shims/unix/foreign_items.rs | 9 +++- src/shims/unix/socket.rs | 48 ++++++++++++++++------ src/shims/unix/socket_address.rs | 14 ++++++- src/shims/unix/tcp_socket.rs | 18 +++++++- src/shims/unix/virtual_socket.rs | 10 ++++- tests/pass-dep/libc/socket-tcp.rs | 68 +++++++++++++++++++++++++++++++ 6 files changed, 149 insertions(+), 18 deletions(-) diff --git a/src/shims/unix/foreign_items.rs b/src/shims/unix/foreign_items.rs index c3af6bfb46..8a58374c68 100644 --- a/src/shims/unix/foreign_items.rs +++ b/src/shims/unix/foreign_items.rs @@ -698,7 +698,14 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> { shim_sig!(extern "C" fn(i32, *_, libc::size_t, i32) -> libc::ssize_t), (link_name, abi, args), )?; - this.recv(socket, buffer, length, flags, dest)?; + this.recv(socket, buffer, length, flags, /* dest_addr */ None, dest)?; + } + "recvfrom" => { + let [socket, buffer, length, flags, dest_addr, dest_addr_len] = &this.check_shim_sig( + shim_sig!(extern "C" fn(i32, *_, libc::size_t, i32, *libc::sockaddr, *libc::socklen_t) -> libc::ssize_t), + (link_name, abi, args), + )?; + this.recv(socket, buffer, length, flags, Some((dest_addr, dest_addr_len)), dest)?; } "setsockopt" => { let [socket, level, option_name, option_value, option_len] = &this.check_shim_sig( diff --git a/src/shims/unix/socket.rs b/src/shims/unix/socket.rs index 2caa8c0d70..a2bc3d9521 100644 --- a/src/shims/unix/socket.rs +++ b/src/shims/unix/socket.rs @@ -65,12 +65,15 @@ pub trait UnixSocketFileDescription: UnixFileDescription { throw_unsup_format!("cannot connect {}", self.name()); } - /// Receive data on the socket into the given buffer `ptr`. + /// Receive data on the socket into the given buffer `ptr` and return the socket + /// address from where the data originated. /// `len` indicates how many bytes we should try to receive. /// `is_peek` specifies whether the receive removes the bytes from the receive buffer /// ([`false`]) or leaves them in the receive buffer ([`true`]). /// `is_non_block` specifies whether the receive operation is non-blocking. - /// After a successful receive, `finish` should be called with the amount of bytes received. + /// After a successful receive, `finish` should be called with a tuple containing the + /// amount of bytes received and the socket address from where the data originated, should + /// it be provided by the protocol. fn recv<'tcx>( self: FileDescriptionRef, _communicate_allowed: bool, @@ -79,7 +82,7 @@ pub trait UnixSocketFileDescription: UnixFileDescription { _is_peek: bool, _is_non_block: bool, _ecx: &mut MiriInterpCx<'tcx>, - _finish: DynMachineCallback<'tcx, Result>, + _finish: DynMachineCallback<'tcx, Result<(usize, Option), IoError>>, ) -> InterpResult<'tcx> { throw_unsup_format!("cannot receive from {}", self.name()); } @@ -432,7 +435,7 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> { // We only attempt a write if the address pointer is not a null pointer. // If the address pointer is a null pointer the user isn't interested in the // address and we don't need to write anything. - ecx.write_socket_address(&address, address_ptr, &address_len_place)?; + ecx.write_socket_address(Some(&address), address_ptr, &address_len_place)?; } // We need to create the scalar using the destination size since @@ -597,6 +600,7 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> { buffer: &OpTy<'tcx>, length: &OpTy<'tcx>, flags: &OpTy<'tcx>, + dest_addr: Option<(&OpTy<'tcx>, &OpTy<'tcx>)>, // Location where the output scalar is written to. dest: &MPlaceTy<'tcx>, ) -> InterpResult<'tcx> { @@ -609,6 +613,12 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> { this.read_scalar(length)?.to_uint(size_layout.size)?.try_into().unwrap(); let mut flags = this.read_scalar(flags)?.to_i32()?; + let dest_addr = match dest_addr { + Some((addr, addr_len)) => + Some((this.read_pointer(addr)?, this.deref_pointer(addr_len)?)), + None => None, + }; + // Get the file handle let Some(fd) = this.machine.fds.get(socket) else { return this.set_errno_and_return_neg1(LibcError("EBADF"), dest); @@ -671,13 +681,27 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> { this, callback!( @capture<'tcx> { + dest_addr: Option<(Pointer, MPlaceTy<'tcx>)>, dest: MPlaceTy<'tcx>, - } |ecx, result: Result| { - match result { - Ok(bytes_sent) => - ecx.write_scalar(Scalar::from_target_usize(bytes_sent.try_into().unwrap(), ecx), &dest), - Err(e) => ecx.set_errno_and_return_neg1(e, &dest) + } |ecx, result: Result<(usize, Option), IoError>| { + let (bytes_received, origin_addr_opt) = match result { + Ok(data) => data, + Err(e) => return ecx.set_errno_and_return_neg1(e, &dest) + }; + + if let Some((addr_ptr, addr_len_mplace)) = dest_addr && addr_ptr != Pointer::null() { + // When the `addr_ptr` is a null pointer the caller is not interested in the + // socket address and we thus don't write anything in that case. + + // The recvfrom(3) man page specifies that "[...] the value stored in the object pointed to + // `address` is unspecified [...]" should the protocol not provide the source address of the message. + // In this case we do the same thing as the Linux implementation and simply don't write anything to the + // `addr_ptr` pointer and zero the `addr_len_mplace` place. + ecx.write_socket_address(origin_addr_opt.as_ref(), addr_ptr, &addr_len_mplace)?; } + + ecx.write_scalar(Scalar::from_target_usize(bytes_received.try_into().unwrap(), ecx), &dest) + } ), ) @@ -820,7 +844,7 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> { Err(e) => return this.set_errno_and_return_neg1_i32(e), }; - this.write_socket_address(&address, address_ptr, &address_len_place) + this.write_socket_address(Some(&address), address_ptr, &address_len_place) .map(|()| Scalar::from_i32(0)) } @@ -864,11 +888,11 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> { }; ecx.write_socket_address( - &address, + Some(&address), address_ptr, &address_len_place, )?; - ecx.write_scalar(Scalar::from_i32(0), &dest) + ecx.write_scalar(Scalar::from_i32(0), &dest) } ), ) diff --git a/src/shims/unix/socket_address.rs b/src/shims/unix/socket_address.rs index 411ceaad75..aca6224586 100644 --- a/src/shims/unix/socket_address.rs +++ b/src/shims/unix/socket_address.rs @@ -244,16 +244,26 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> { /// on output it points to the non-truncated size of the written address in the /// buffer pointed to by `address_ptr`. /// + /// If `address` is [`None`] we write an "empty" address (i.e., the `address_len_ptr` + /// is set to zero and the `address_ptr` is left untouched). + /// /// If the address buffer doesn't fit the whole address, the address is truncated to not /// overflow the buffer. fn write_socket_address( &mut self, - address: &SocketAddr, + address: Option<&SocketAddr>, address_ptr: Pointer, address_len_place: &MPlaceTy<'tcx>, ) -> InterpResult<'tcx> { let this = self.eval_context_mut(); + let Some(address) = address else { + // We want to write an "empty" socket address. In this case we just set + // the address length to zero. + + return this.write_int(0, address_len_place); + }; + // We only support buffer lengths which can be stored in a u64 since the // size of a layout in bytes is also stored in a u64. let address_buffer_len: u64 = this @@ -446,7 +456,7 @@ trait EvalContextPrivExt<'tcx>: crate::MiriInterpCxExt<'tcx> { this.write_int(sockaddr_layout.size.bytes(), &addrlen_mplace)?; let sockaddr_mplace = this.allocate(sockaddr_layout, MiriMemoryKind::Machine.into())?; - this.write_socket_address(&address, sockaddr_mplace.ptr(), &addrlen_mplace)?; + this.write_socket_address(Some(&address), sockaddr_mplace.ptr(), &addrlen_mplace)?; this.write_pointer(sockaddr_mplace.ptr(), &addr_mplace)?; let canonname_mplace = this.project_field_named(&addrinfo_mplace, "ai_canonname")?; diff --git a/src/shims/unix/tcp_socket.rs b/src/shims/unix/tcp_socket.rs index 3ebb5a92f4..69d88f077f 100644 --- a/src/shims/unix/tcp_socket.rs +++ b/src/shims/unix/tcp_socket.rs @@ -170,7 +170,13 @@ impl FileDescription for TcpSocket { /* is_peek */ false, /* is_non_block */ false, ecx, - finish, + callback!( + @capture<'tcx> { + finish: DynMachineCallback<'tcx, Result>, + } |this, result: Result<(usize, Option), IoError>| { + finish.call(this, result.map(|(bytes, _)| bytes)) + } + ), ) } @@ -645,13 +651,21 @@ impl UnixSocketFileDescription for TcpSocket { is_peek: bool, is_non_block: bool, ecx: &mut MiriInterpCx<'tcx>, - finish: DynMachineCallback<'tcx, Result>, + finish: DynMachineCallback<'tcx, Result<(usize, Option), IoError>>, ) -> InterpResult<'tcx> { assert!(communicate_allowed, "cannot have `TcpSocket` with isolation enabled!"); let is_non_block = is_non_block || self.is_non_block.get(); let deadline = ecx.action_deadline(is_non_block, self.read_timeout.get()); + let finish = callback!( + @capture<'tcx> { + finish: DynMachineCallback<'tcx, Result<(usize, Option), IoError>>, + } |this, result: Result| { + // The source address of the data isn't available for stream-based sockets. + finish.call(this, result.map(|bytes| (bytes, None))) + } + ); let socket = self; ecx.ensure_connected( socket.clone(), diff --git a/src/shims/unix/virtual_socket.rs b/src/shims/unix/virtual_socket.rs index a192321c92..60f3eb88d6 100644 --- a/src/shims/unix/virtual_socket.rs +++ b/src/shims/unix/virtual_socket.rs @@ -291,12 +291,20 @@ impl UnixSocketFileDescription for VirtualSocket { is_peek: bool, is_non_block: bool, ecx: &mut MiriInterpCx<'tcx>, - finish: DynMachineCallback<'tcx, Result>, + finish: DynMachineCallback<'tcx, Result<(usize, Option), IoError>>, ) -> InterpResult<'tcx> { if is_peek { throw_unsup_format!("socketpair: virtual sockets don't support peeking") } + let finish = callback!( + @capture<'tcx> { + finish: DynMachineCallback<'tcx, Result<(usize, Option), IoError>>, + } |this, result: Result| { + // The source address of the data isn't available for stream-based sockets. + finish.call(this, result.map(|bytes| (bytes, None))) + } + ); ecx.virtual_socket_read(self, ptr, len, is_non_block, finish) } } diff --git a/tests/pass-dep/libc/socket-tcp.rs b/tests/pass-dep/libc/socket-tcp.rs index 5f1fcfb9ce..bfc8e5f4e3 100644 --- a/tests/pass-dep/libc/socket-tcp.rs +++ b/tests/pass-dep/libc/socket-tcp.rs @@ -40,6 +40,7 @@ fn main() { test_connect_listening(); test_send_peek_recv(); test_write_read(); + test_recvfrom(); test_sendto(); test_readv(); test_writev(); @@ -399,6 +400,73 @@ fn test_write_read() { server_thread.join().unwrap(); } +/// Test receiving data from a TCP socket using `recvfrom`. +/// Since TCP is a connection-mode protocol the `address` and `address_len` +/// pointers should be left unchanged. +fn test_recvfrom() { + let (server_sockfd, addr) = net::make_listener_ipv4().unwrap(); + let client_sockfd = + unsafe { errno_result(libc::socket(libc::AF_INET, libc::SOCK_STREAM, 0)).unwrap() }; + + // Spawn the server thread. + let server_thread = thread::spawn(move || { + let (peerfd, _) = net::accept_ipv4(server_sockfd).unwrap(); + + // Write some bytes into the stream. + + libc_utils::write_all(peerfd, TEST_BYTES).unwrap(); + libc_utils::write_all(peerfd, TEST_BYTES).unwrap(); + }); + + net::connect_ipv4(client_sockfd, addr).unwrap(); + + if cfg!(miri) || !cfg!(any(target_os = "freebsd", target_vendor = "apple")) { + // Native FreeBSD and macOS return EFAULT when the `address_len` pointer is invalid. + // This is not documented in their man pages and we thus skip those native tests. + + let mut buffer = [0; TEST_BYTES.len()]; + + // When the address pointer is a nullptr, the `address_len` should be left unchanged. + unsafe { + errno_result(libc::recvfrom( + client_sockfd, + buffer.as_mut_ptr().cast(), + buffer.len(), + /* flags */ 0, + ptr::null_mut::(), + ptr::without_provenance_mut::(0x200), + )) + .unwrap(); + } + + assert_eq!(&buffer, TEST_BYTES); + } + + let mut buffer: [u8; 26] = [0; TEST_BYTES.len()]; + let mut address_ptr_len = 1337 as libc::socklen_t; + + // If address pointer is non-null, `address_len` should be zeroed (but address itself is not accessed). + // We thus use a garbage value for the address pointer to ensure it's ignored. + unsafe { + errno_result(libc::recvfrom( + client_sockfd, + buffer.as_mut_ptr().cast(), + buffer.len(), + /* flags */ 0, + ptr::without_provenance_mut::(16), + &mut address_ptr_len, + )) + .unwrap(); + } + + assert_eq!(&buffer, TEST_BYTES); + + // Ensure the `address_len` pointer gets zeroed. + assert_eq!(address_ptr_len, 0); + + server_thread.join().unwrap(); +} + /// Test sending data into a TCP socket using `sendto`. /// We provide a nullptr as destination address, which should not be /// a problem since TCP is a connection-mode protocol and thus the From 1c5b05d8fad53322df6afb2127e23d456f2cc38a Mon Sep 17 00:00:00 2001 From: Ralf Jung Date: Fri, 6 Nov 2026 18:00:22 +0100 Subject: [PATCH 3/3] recvfrom, sendto: require given address length to be fully dereferenceable --- src/shims/unix/socket.rs | 22 +++++++++----- src/shims/unix/socket_address.rs | 37 ++++++++++++++++++++---- tests/pass-dep/libc/socket-tcp.rs | 48 ++++++++++++++----------------- tests/pass-dep/libc/socket.rs | 4 ++- 4 files changed, 70 insertions(+), 41 deletions(-) diff --git a/src/shims/unix/socket.rs b/src/shims/unix/socket.rs index a2bc3d9521..185646c16e 100644 --- a/src/shims/unix/socket.rs +++ b/src/shims/unix/socket.rs @@ -519,10 +519,16 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> { this.read_scalar(dest_addr_len)?.to_uint(socklen_layout.size)?; if dest_addr_ptr == Pointer::null() || dest_addr_len_int == 0 { - // If the address pointer is a nullptr or the address length is zero - // we ignore the destination address. + // If the address pointer is a nullptr or the address length is zero we ignore + // the destination address. BSDs return EINVAL when the address length is zero + // and the address pointer is an invalid pointer; that is so far away from what + // POSIX documents for TCP sockets that we do not try to emulate it. None } else { + // If they are both non-null/zero, we read the address even if the socket does + // not care about it. This is stricter than what POSIX says (POSIX says the + // destination is ignored for TCP sockets), but most OSes seem to do something + // similar. match this.read_socket_address(dest_addr, dest_addr_len, "sendto")? { Ok(dest_addr) => Some(dest_addr), Err(e) => return this.set_errno_and_return_neg1(e, dest), @@ -684,15 +690,17 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> { dest_addr: Option<(Pointer, MPlaceTy<'tcx>)>, dest: MPlaceTy<'tcx>, } |ecx, result: Result<(usize, Option), IoError>| { - let (bytes_received, origin_addr_opt) = match result { + let (bytes_received, mut origin_addr_opt) = match result { Ok(data) => data, Err(e) => return ecx.set_errno_and_return_neg1(e, &dest) }; - if let Some((addr_ptr, addr_len_mplace)) = dest_addr && addr_ptr != Pointer::null() { - // When the `addr_ptr` is a null pointer the caller is not interested in the - // socket address and we thus don't write anything in that case. - + if let Some((addr_ptr, addr_len_mplace)) = dest_addr { + if addr_ptr == Pointer::null() { + // When the `addr_ptr` is a null pointer the caller is not interested in + // the socket address. + origin_addr_opt = None; + } // The recvfrom(3) man page specifies that "[...] the value stored in the object pointed to // `address` is unspecified [...]" should the protocol not provide the source address of the message. // In this case we do the same thing as the Linux implementation and simply don't write anything to the diff --git a/src/shims/unix/socket_address.rs b/src/shims/unix/socket_address.rs index aca6224586..64ec52e2e5 100644 --- a/src/shims/unix/socket_address.rs +++ b/src/shims/unix/socket_address.rs @@ -137,9 +137,16 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> { // size of a layout is also stored in a u64. let address_len: u64 = this.read_scalar(address_len)?.to_int(address_len.layout.size)?.try_into().unwrap(); + let address = this.deref_pointer(address)?; + + // Ensure the pointer is actually dereferenceable for that length. + this.check_ptr_access( + address.ptr(), + Size::from_bytes(address_len), + CheckInAllocMsg::MemoryAccess, + )?; // Initially, treat address as generic sockaddr just to extract the family field. - let address = this.deref_pointer(address)?; if address_len < address.layout.size.bytes() { // Address length should be at least as big as the generic sockaddr return interp_ok(Err(LibcError("EINVAL"))); @@ -246,6 +253,7 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> { /// /// If `address` is [`None`] we write an "empty" address (i.e., the `address_len_ptr` /// is set to zero and the `address_ptr` is left untouched). + /// If `address` is `Some`, both output pointers must be valid! /// /// If the address buffer doesn't fit the whole address, the address is truncated to not /// overflow the buffer. @@ -257,12 +265,16 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> { ) -> InterpResult<'tcx> { let this = self.eval_context_mut(); - let Some(address) = address else { - // We want to write an "empty" socket address. In this case we just set - // the address length to zero. + if address.is_none() && this.ptr_is_null(address_len_place.ptr())? { + // It is okay for the len ptr to be null *if* we have nothing to write. + return interp_ok(()); + } - return this.write_int(0, address_len_place); - }; + // If the len ptr is not null or if we have an address to write, the length must be valid. + // This is intended to implement what POSIX says about the length pointer in `recvfrom``: + // > Either a null pointer, if address is a null pointer, or a pointer to a socklen_t object + // > which on input specifies the length of the supplied sockaddr structure, and on output + // > specifies the length of the sending address. // We only support buffer lengths which can be stored in a u64 since the // size of a layout in bytes is also stored in a u64. @@ -272,6 +284,19 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> { .try_into() .unwrap(); + // Check that the buffer indeed has (at least) the given size. + this.check_ptr_access( + address_ptr, + Size::from_bytes(address_buffer_len), + CheckInAllocMsg::MemoryAccess, + )?; + + let Some(address) = address else { + // We want to write an "empty" socket address. In this case we just set + // the address length to zero. + return this.write_int(0, address_len_place); + }; + let address_buffer = match address { SocketAddr::V4(address) => { // IPv4 address bytes; already stored in network byte order. diff --git a/tests/pass-dep/libc/socket-tcp.rs b/tests/pass-dep/libc/socket-tcp.rs index bfc8e5f4e3..74b62aab0f 100644 --- a/tests/pass-dep/libc/socket-tcp.rs +++ b/tests/pass-dep/libc/socket-tcp.rs @@ -9,7 +9,7 @@ mod utils; use std::io::ErrorKind; use std::time::{Duration, Instant}; -use std::{ptr, thread}; +use std::{mem, ptr, thread}; use libc_utils::*; @@ -420,50 +420,44 @@ fn test_recvfrom() { net::connect_ipv4(client_sockfd, addr).unwrap(); - if cfg!(miri) || !cfg!(any(target_os = "freebsd", target_vendor = "apple")) { - // Native FreeBSD and macOS return EFAULT when the `address_len` pointer is invalid. - // This is not documented in their man pages and we thus skip those native tests. - - let mut buffer = [0; TEST_BYTES.len()]; - - // When the address pointer is a nullptr, the `address_len` should be left unchanged. - unsafe { - errno_result(libc::recvfrom( - client_sockfd, - buffer.as_mut_ptr().cast(), - buffer.len(), - /* flags */ 0, - ptr::null_mut::(), - ptr::without_provenance_mut::(0x200), - )) - .unwrap(); - } + let mut buffer = [0; TEST_BYTES.len()]; - assert_eq!(&buffer, TEST_BYTES); + // It is legal to make both the addr and addr_len pointers null. + unsafe { + errno_result(libc::recvfrom( + client_sockfd, + buffer.as_mut_ptr().cast(), + buffer.len(), + /* flags */ 0, + ptr::null_mut::(), + ptr::null_mut::(), + )) + .unwrap(); } + assert_eq!(&buffer, TEST_BYTES); + let mut buffer: [u8; 26] = [0; TEST_BYTES.len()]; - let mut address_ptr_len = 1337 as libc::socklen_t; // If address pointer is non-null, `address_len` should be zeroed (but address itself is not accessed). - // We thus use a garbage value for the address pointer to ensure it's ignored. + let mut addr = unsafe { mem::zeroed::() }; + let mut addr_len = mem::size_of_val(&addr) as libc::socklen_t; unsafe { errno_result(libc::recvfrom( client_sockfd, buffer.as_mut_ptr().cast(), buffer.len(), /* flags */ 0, - ptr::without_provenance_mut::(16), - &mut address_ptr_len, + &mut addr, + &mut addr_len, )) .unwrap(); } + // Ensure the `address_len` pointer gets zeroed. + assert_eq!(addr_len, 0); assert_eq!(&buffer, TEST_BYTES); - // Ensure the `address_len` pointer gets zeroed. - assert_eq!(address_ptr_len, 0); - server_thread.join().unwrap(); } diff --git a/tests/pass-dep/libc/socket.rs b/tests/pass-dep/libc/socket.rs index a8ed32066d..f5ab0b2ccb 100644 --- a/tests/pass-dep/libc/socket.rs +++ b/tests/pass-dep/libc/socket.rs @@ -116,10 +116,12 @@ fn test_bind_ipv4_invalid_addr_len() { // A too big size is also invalid. Some native hosts (e.g. Linux) // allow too big sizes, so we skip this test on native hosts: // + // Make sure the bigger size is still fully in-bounds. + let addrs = [addr; 2]; let err = unsafe { errno_result(libc::bind( sockfd, - (&addr as *const libc::sockaddr_in).cast::(), + addrs.as_ptr().cast::(), (size_of::() + 1) as libc::socklen_t, )) .unwrap_err()