Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement basic support for sockets #3449

Open
RalfJung opened this issue Apr 4, 2024 · 9 comments
Open

Implement basic support for sockets #3449

RalfJung opened this issue Apr 4, 2024 · 9 comments
Labels
A-files Area: related to files, paths, sockets, file descriptors, or handles A-shims Area: This affects the external function shims A-unix Area: affects our shared Unix target support C-project Category: a larger project is being tracked here, usually with checkmarks for individual steps

Comments

@RalfJung
Copy link
Member

RalfJung commented Apr 4, 2024

Miri can access files but not sockets currently.

The main issue with implementing blocking sockets is that they would provide the first source of blocking in Miri that is controlled by the outside world (except for time but we've got that handled pretty well). That will be quite non-trivial to implement as Miri needs to basically become or import an async runtime. We'll have a concept of threads being blocked on a socket. When all threads are blocked and Miri goes to sleep to wait for a timeout to pass, it needs to be able to wait on "either the timeout passes or all the sockets any thread is blocked on". When the socket unblocks we should wake up the thread, even if we never reach the "all threads are blocked" state. Both of these are exactly the core operations of an async runtime.

Somewhat surprisingly, I do not think that epoll makes this a lot more complicated. Even without epoll, we could have 5 threads waiting on a different socket each, so Miri basically needs epoll on the host even if Miri programs do not have epoll available. Having epoll just means that a single Miri thread can wait on more than one socket, but since Miri anyway has to support many threads that doesn't add significant new complications.

Non-blocking sockets would be a lot simpler, but probably also a lot less interesting.

@RalfJung RalfJung added C-project Category: a larger project is being tracked here, usually with checkmarks for individual steps A-shims Area: This affects the external function shims labels Apr 4, 2024
@RalfJung RalfJung added A-files Area: related to files, paths, sockets, file descriptors, or handles A-unix Area: affects our shared Unix target support labels May 5, 2024
@RalfJung
Copy link
Member Author

RalfJung commented May 5, 2024

As discussed on Zulip, it would probably make sense to use mio on the Miri side here. Fundamentally, the primitives we need are:

  • each time the active thread yields, do a non-blocking poll on a set of host sockets -- that's generalizing what we already do here where we check whether there's a timer we need to fire
  • if all Miri threads are blocked, do a blocking poll on a set of host sockets (potentially with a timeout) -- that's generalizing what we do here and here where we just sleep until the next timeout expires

The hope is that mio would give us both of these in a platform-independent way, so that we can run on Windows hosts. (Note that this issue is only about Unix targets, using the typical POSIX socket API. Support for sockets on Windows targets is out-of-scope here. But if possible we should support running Unix targets on Windows hosts, as we already do with file system access.)

@oli-obk
Copy link
Contributor

oli-obk commented Jul 26, 2024

There's also a dumb way to do this: just spawn a physical host thread whenever we want to do a blocking op and poll that thread every now and then to see if it finished.

@jonleivent
Copy link

Just so you know that someone would use socket support when it gets put in.

error: unsupported operation: can't call foreign function `socket` on OS `linux`
   --> /home/jil/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/std/src/sys/pal/unix/net.rs:88:34
    |
88  |                     let fd = cvt(libc::socket(fam, ty | libc::SOCK_CLOEXEC, 0))?;
    |                                  ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ can't call foreign function `socket` on OS `linux`
    |
    = help: if this is a basic API commonly used on this target, please report an issue with Miri
    = help: however, note that Miri does not aim to support every FFI function out there; for instance, we will not support APIs for things such as GUIs, scripting languages, or databases
    = note: BACKTRACE:
    = note: inside `std::sys::pal::unix::net::Socket::new_raw` at /home/jil/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/std/src/sys/pal/unix/net.rs:88:34: 88:79
    = note: inside `std::os::unix::net::UnixListener::bind::<&std::path::PathBuf>` at /home/jil/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/std/src/os/unix/net/listener.rs:74:25: 74:74

@onkoe
Copy link

onkoe commented Jan 31, 2025

Same here! I had an example using void ptrs resulting in an IO Safety Violation error. It would've been very helpful to use Miri to see how and why the safety violation occurred!

test thread::tests::make_socket ... error: unsupported operation: can't call foreign function `socket` on OS `linux`
   --> /home/barrett/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/std/src/sys/pal/unix/net.rs:89:34
    |
89  |                     let fd = cvt(libc::socket(fam, ty | libc::SOCK_CLOEXEC, 0))?;
    |                                  ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ can't call foreign function `socket` on OS `linux`
    |
    = help: if this is a basic API commonly used on this target, please report an issue with Miri
    = help: however, note that Miri does not aim to support every FFI function out there; for instance, we will not support APIs for things such as GUIs, scripting languages, or databases
    = note: BACKTRACE on thread `thread::tests::`:
    = note: inside `std::sys::pal::unix::net::Socket::new_raw` at /home/barrett/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/std/src/sys/pal/unix/net.rs:89:34: 89:79
    = note: inside `std::sys::pal::unix::net::Socket::new` at /home/barrett/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/std/src/sys/pal/unix/net.rs:68:9: 68:33
    = note: inside `std::sys_common::net::UdpSocket::bind` at /home/barrett/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/std/src/sys_common/net.rs:524:20: 524:52
    = note: inside `<for<'a> fn(std::result::Result<&'a std::net::SocketAddr, std::io::Error>) -> std::result::Result<std::sys_common::net::UdpSocket, std::io::Error> {std::sys_common::net::UdpSocket::bind} as std::ops::FnMut<(std::result::Result<&std::net::SocketAddr, std::io::Error>,)>>::call_mut - shim(for<'a> fn(std::result::Result<&'a std::net::SocketAddr, std::io::Error>) -> std::result::Result<std::sys_common::net::UdpSocket, std::io::Error> {std::sys_common::net::UdpSocket::bind})` at /home/barrett/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/ops/function.rs:166:5: 166:75
    = note: inside `std::net::each_addr::<&str, for<'a> fn(std::result::Result<&'a std::net::SocketAddr, std::io::Error>) -> std::result::Result<std::sys_common::net::UdpSocket, std::io::Error> {std::sys_common::net::UdpSocket::bind}, std::sys_common::net::UdpSocket>` at /home/barrett/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/std/src/net/mod.rs:81:15: 81:27
    = note: inside `std::net::UdpSocket::bind::<&str>` at /home/barrett/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/std/src/net/udp.rs:121:9: 121:57
note: inside `thread::tests::make_socket`
   --> src/thread.rs:350:31
    |
350 |             let some_socket = UdpSocket::bind("127.0.0.1:9999").unwrap();
    |                               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
note: inside closure
   --> src/thread.rs:339:21
    |
338 |     #[test] // TODO: run through miri!
    |     ------- in this procedural macro expansion
339 |     fn make_socket() {
    |                     ^
    = note: this error originates in the attribute macro `test` (in Nightly builds, run with -Z macro-backtrace for more info)

note: some details are omitted, run with `MIRIFLAGS=-Zmiri-backtrace=full` for a verbose backtrace

error: aborting due to 1 previous error; 27 warnings emitted

error: test failed, to rerun pass `--lib`

Caused by:
  process didn't exit successfully: `/home/barrett/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/bin/cargo-miri runner /home/barrett/Documents/projects/Rover/gps-rs/target/miri/x86_64-unknown-linux-gnu/debug/deps/soro_gps-9208bcd2dc9e9e26 make_socket` (exit status: 1)
note: test exited abnormally; to see the full output pass --nocapture to the harness.
barrett@farts ~/D/p/R/gps-rs (feat/thread) [1]> 

Testing normally yielded the following output:

running 1 test
2025-01-31T15:21:17.410664Z DEBUG soro_gps::thread::tests: on iteration 0, read: `[]`
fatal runtime error: IO Safety violation: owned file descriptor already closed
error: test failed, to rerun pass `-p soro_gps --lib`

...and as such, I can kinda understand why this hasn't been built yet.

Implementing this feature by connecting to the system prevents doing safety checks on libc-level invariants, like, "make sure you wrap sockets in a ManuallyDrop or the file descriptor will be released when the cast'd type falls out of scope." On the other hand, the hard way has a lot to do with checking libc's model - a lot less to do with Rust.

@RalfJung
Copy link
Member Author

Thanks for sharing! We are well aware that this is a feature folks run into regularly, no need to add further user stories here. :)

However, what would be useful is getting an idea of whether TCP or UDP should be the first one to tackle. I kind of assume TCP is more common so that is what I would default to... but it's also the bigger API surface.^^

@jonleivent
Copy link

My use case is basic Unix stream socket (Wayland), so not TCP. It does use epoll.

@RalfJung
Copy link
Member Author

Oh right, there's a third kind of socket... and it seems like there is a wrapper for that in std, albeit only for Unix hosts. Bummer that this breaks Miri's usual cross-platform principle but I guess there's not much we can do about that.

I hope you don't need to attach FDs to messages as I don't think that is anywhere near the top of the roadmap. We'll start with very basic read and write only.

@jonleivent
Copy link

jonleivent commented Jan 31, 2025

I hope you don't need to attach FDs to messages...

Definitely do need to. So you're saying it's going to be a while ;) Please don't prioritize my specific use case ahead of what you think are the more important and easier cases to tackle.

@RalfJung
Copy link
Member Author

Yeah I'm afraid that will be a while.^^

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-files Area: related to files, paths, sockets, file descriptors, or handles A-shims Area: This affects the external function shims A-unix Area: affects our shared Unix target support C-project Category: a larger project is being tracked here, usually with checkmarks for individual steps
Projects
None yet
Development

No branches or pull requests

4 participants