diff --git a/.github/workflows/cargo-publish.yml b/.github/workflows/cargo-publish.yml index bb83372..e4c6fbf 100644 --- a/.github/workflows/cargo-publish.yml +++ b/.github/workflows/cargo-publish.yml @@ -1,6 +1,8 @@ name: Publish to crates.io on: + release: + types: [published] workflow_dispatch: inputs: dry_run: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 0cba6e8..c846d4e 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -56,10 +56,3 @@ jobs: run: | VERSION="${{ steps.version.outputs.version }}" gh workflow run publish-examples.yml --ref "v$VERSION" - - - name: Trigger crates.io publish - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - VERSION="${{ steps.version.outputs.version }}" - gh workflow run cargo-publish.yml --ref "v$VERSION" -f dry_run=false diff --git a/.github/workflows/test-examples.yml b/.github/workflows/test-examples.yml index d372ee3..1b45e75 100644 --- a/.github/workflows/test-examples.yml +++ b/.github/workflows/test-examples.yml @@ -223,6 +223,9 @@ jobs: - example: networking-py args: "--net -- /urllib_get.py" expect: "SUCCESS: urllib GET worked!" + - example: networking-py + args: "--net -- /urllib_get_no_timeout.py" + expect: "SUCCESS: urllib GET \\(no timeout\\) worked!" - example: networking-py args: "--port 8080 -- /echo_server_test.py" expect: "SUCCESS: bind\\+listen on port 8080 allowed" @@ -613,6 +616,9 @@ jobs: - example: networking-py args: "--net -- /urllib_get.py" expect: "SUCCESS: urllib GET worked!" + - example: networking-py + args: "--net -- /urllib_get_no_timeout.py" + expect: "SUCCESS: urllib GET \\(no timeout\\) worked!" - example: networking-py args: "--port 8080 -- /echo_server_test.py" expect: "SUCCESS: bind\\+listen on port 8080 allowed" diff --git a/examples/networking-py/Dockerfile b/examples/networking-py/Dockerfile index c16ba5b..b550171 100644 --- a/examples/networking-py/Dockerfile +++ b/examples/networking-py/Dockerfile @@ -8,6 +8,7 @@ FROM ${BASE} AS rootfs COPY http_get.py /http_get.py COPY echo_server.py /echo_server.py COPY urllib_get.py /urllib_get.py +COPY urllib_get_no_timeout.py /urllib_get_no_timeout.py COPY https_test.py /https_test.py COPY echo_server_test.py /echo_server_test.py diff --git a/examples/networking-py/urllib_get_no_timeout.py b/examples/networking-py/urllib_get_no_timeout.py new file mode 100644 index 0000000..1d8c27e --- /dev/null +++ b/examples/networking-py/urllib_get_no_timeout.py @@ -0,0 +1,25 @@ +"""HTTP GET using urllib WITHOUT an explicit timeout. + +Same as urllib_get.py but omits the timeout= argument to urlopen(). +This exercises the code path used by mxc, where the Unikraft guest +kernel busy-polls net_poll with timeout_ms=0 and relies on the host +to detect read-readiness via select(). +""" +import urllib.request +import sys + +URL = "http://example.com/" + +print(f"Fetching {URL} (no timeout) ...") +try: + with urllib.request.urlopen(URL) as resp: + body = resp.read().decode("utf-8", errors="replace") + print(f"Status: {resp.status}") + print(f"Body length: {len(body)} bytes") + if "Example Domain" in body: + print("SUCCESS: urllib GET (no timeout) worked!") + else: + print("WARNING: unexpected body content") +except Exception as e: + print(f"FAILED: {e}") + sys.exit(1) diff --git a/host/Cargo.lock b/host/Cargo.lock index d86735d..3e00434 100644 --- a/host/Cargo.lock +++ b/host/Cargo.lock @@ -1129,9 +1129,9 @@ checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" [[package]] name = "hybrid-array" -version = "0.4.12" +version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9155a582abd142abc056962c29e3ce5ff2ad5469f4246b537ed42c5deba857da" +checksum = "818356c5132c1fede50f837ca96afbe78ff42413047f4abb886217845e1b6c8c" dependencies = [ "typenum", ] @@ -1204,7 +1204,7 @@ dependencies = [ [[package]] name = "hyperlight-unikraft" -version = "0.11.0" +version = "0.11.1" dependencies = [ "anyhow", "base64", @@ -1916,9 +1916,9 @@ checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" [[package]] name = "rapidhash" -version = "4.4.1" +version = "4.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5e48930979c155e2f33aa36ab3119b5ee81332beb6482199a8ecd6029b80b59" +checksum = "32b266a82f4aa99bb5c25e28d11cc44ace63d91adbcbcee4d323e2ae3d49ef37" dependencies = [ "rustversion", ] diff --git a/host/Cargo.toml b/host/Cargo.toml index 8c04c31..601cdd6 100644 --- a/host/Cargo.toml +++ b/host/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "hyperlight-unikraft" -version = "0.11.0" +version = "0.11.1" edition = "2021" description = "Embedded Hyperlight host for running Unikraft unikernels" license = "MIT OR Apache-2.0" diff --git a/host/src/lib.rs b/host/src/lib.rs index d2bdf16..4ca9c63 100644 --- a/host/src/lib.rs +++ b/host/src/lib.rs @@ -1315,6 +1315,13 @@ fn handle_net_connect( let tbl = table.lock().unwrap(); let sock = tbl.get_socket(fd)?; sock.connect_timeout(&sa, SOCKET_TIMEOUT)?; + // connect_timeout() internally sets nonblocking(false); restore + // the read/write timeouts that it may have cleared. + #[cfg(windows)] + { + sock.set_read_timeout(Some(SOCKET_TIMEOUT))?; + sock.set_write_timeout(Some(SOCKET_TIMEOUT))?; + } Ok(json!({})) } @@ -1672,11 +1679,21 @@ fn handle_net_poll( return Ok(json!({"ready": ready})); } + // The Unikraft guest may busy-poll with timeout_ms=0 without + // yielding via __hl_sleep. Enforce a minimum so poll() has time + // to detect incoming data that arrives between poll cycles. + let has_pollin = pollfds.iter().any(|p| p.events & libc::POLLIN != 0); + let effective_ms = if has_pollin { + timeout_ms.max(50) + } else { + timeout_ms + }; + let ret = unsafe { libc::poll( pollfds.as_mut_ptr(), pollfds.len() as libc::nfds_t, - timeout_ms, + effective_ms, ) }; @@ -1706,47 +1723,14 @@ fn handle_net_poll( use serde_json::json; use std::os::windows::io::AsRawSocket; use windows_sys::Win32::Networking::WinSock::{ - WSAPoll, POLLERR as W_POLLERR, POLLHUP as W_POLLHUP, POLLNVAL as W_POLLNVAL, POLLRDNORM, - POLLWRNORM, WSAPOLLFD, + __WSAFDIsSet, select, FD_SET, FD_SETSIZE, SOCKET, SOCKET_ERROR, TIMEVAL, }; const POSIX_POLLIN: i16 = 0x0001; const POSIX_POLLOUT: i16 = 0x0004; const POSIX_POLLERR: i16 = 0x0008; - const POSIX_POLLHUP: i16 = 0x0010; const POSIX_POLLNVAL: i16 = 0x0020; - fn posix_to_win(posix: i16) -> i16 { - let mut win: i16 = 0; - if posix & POSIX_POLLIN != 0 { - win |= POLLRDNORM; - } - if posix & POSIX_POLLOUT != 0 { - win |= POLLWRNORM; - } - win - } - - fn win_to_posix(win: i16) -> i16 { - let mut posix: i16 = 0; - if win & POLLRDNORM != 0 { - posix |= POSIX_POLLIN; - } - if win & POLLWRNORM != 0 { - posix |= POSIX_POLLOUT; - } - if win & W_POLLERR != 0 { - posix |= POSIX_POLLERR; - } - if win & W_POLLHUP != 0 { - posix |= POSIX_POLLHUP; - } - if win & W_POLLNVAL != 0 { - posix |= POSIX_POLLNVAL; - } - posix - } - let fds_val = args .get("fds") .and_then(|v| v.as_array()) @@ -1758,8 +1742,15 @@ fn handle_net_poll( .clamp(0, i32::MAX as i64) as i32; let tbl = table.lock().unwrap(); - let mut pollfds: Vec = Vec::new(); - let mut guest_fds: Vec = Vec::new(); + + struct FdEntry { + raw: SOCKET, + guest_fd: u64, + want_read: bool, + want_write: bool, + } + + let mut entries: Vec = Vec::new(); let mut ready = Vec::new(); for entry in fds_val { @@ -1771,36 +1762,98 @@ fn handle_net_poll( if !(0..=i16::MAX as i64).contains(&raw_events) { return Err(anyhow!("net_poll: events {raw_events} out of i16 range")); } - let events = posix_to_win(raw_events as i16); + let events = raw_events as i16; if let Ok(sock) = tbl.get_socket(fd) { - pollfds.push(WSAPOLLFD { - fd: sock.as_raw_socket() as usize, - events, - revents: 0, + entries.push(FdEntry { + raw: sock.as_raw_socket() as SOCKET, + guest_fd: fd, + want_read: events & POSIX_POLLIN != 0, + want_write: events & POSIX_POLLOUT != 0, }); - guest_fds.push(fd); } else { ready.push(json!({ "fd": fd, "revents": POSIX_POLLNVAL as i64 })); } } drop(tbl); - if pollfds.is_empty() { + if entries.is_empty() { return Ok(json!({"ready": ready})); } + if entries.len() > FD_SETSIZE as usize { + return Err(anyhow!("net_poll: too many fds for select()")); + } - let ret = unsafe { WSAPoll(pollfds.as_mut_ptr(), pollfds.len() as u32, timeout_ms) }; + let mut readfds: FD_SET = unsafe { std::mem::zeroed() }; + let mut writefds: FD_SET = unsafe { std::mem::zeroed() }; + let mut exceptfds: FD_SET = unsafe { std::mem::zeroed() }; - if ret < 0 { + for e in &entries { + if e.want_read { + let c = readfds.fd_count as usize; + readfds.fd_array[c] = e.raw; + readfds.fd_count += 1; + } + if e.want_write { + let c = writefds.fd_count as usize; + writefds.fd_array[c] = e.raw; + writefds.fd_count += 1; + } + let c = exceptfds.fd_count as usize; + exceptfds.fd_array[c] = e.raw; + exceptfds.fd_count += 1; + } + + // The Unikraft guest may busy-poll with timeout_ms=0 without + // yielding via __hl_sleep. Enforce a minimum so select() has time + // to detect incoming data that arrives between poll cycles. + let effective_ms = if readfds.fd_count > 0 { + timeout_ms.max(50) + } else { + timeout_ms + }; + let tv = TIMEVAL { + tv_sec: effective_ms / 1000, + tv_usec: (effective_ms % 1000) * 1000, + }; + + let ret = unsafe { + select( + 0, + if readfds.fd_count > 0 { + &mut readfds + } else { + std::ptr::null_mut() + }, + if writefds.fd_count > 0 { + &mut writefds + } else { + std::ptr::null_mut() + }, + &mut exceptfds, + &tv, + ) + }; + + if ret == SOCKET_ERROR { let err = std::io::Error::last_os_error(); - return Err(anyhow!("net_poll: WSAPoll() failed: {err}")); + return Err(anyhow!("net_poll: select() failed: {err}")); } - for (i, pfd) in pollfds.iter().enumerate() { - if pfd.revents != 0 { + for e in &entries { + let mut revents: i16 = 0; + if unsafe { __WSAFDIsSet(e.raw, &mut readfds) } != 0 { + revents |= POSIX_POLLIN; + } + if unsafe { __WSAFDIsSet(e.raw, &mut writefds) } != 0 { + revents |= POSIX_POLLOUT; + } + if unsafe { __WSAFDIsSet(e.raw, &mut exceptfds) } != 0 { + revents |= POSIX_POLLERR; + } + if revents != 0 { ready.push(json!({ - "fd": guest_fds[i], - "revents": win_to_posix(pfd.revents) as i64, + "fd": e.guest_fd, + "revents": revents as i64, })); } } @@ -1824,7 +1877,7 @@ fn hl_sleep_poll_sockets( .values() .map(|hs| libc::pollfd { fd: hs.socket.as_raw_fd(), - events: libc::POLLIN | libc::POLLOUT | libc::POLLERR, + events: libc::POLLIN | libc::POLLOUT, revents: 0, }) .collect(); @@ -1851,7 +1904,8 @@ fn hl_sleep_poll_sockets( } } - Ok(json!({"socket_ready": ret > 0})) + let ready = pollfds.iter().any(|p| p.revents != 0); + Ok(json!({"socket_ready": ready})) } #[cfg(windows)] @@ -1862,35 +1916,54 @@ fn hl_sleep_poll_sockets( ) -> Result { use serde_json::json; use std::os::windows::io::AsRawSocket; - use windows_sys::Win32::Networking::WinSock::{WSAPoll, POLLRDNORM, POLLWRNORM, WSAPOLLFD}; + use windows_sys::Win32::Networking::WinSock::{ + select, FD_SET, FD_SETSIZE, SOCKET, SOCKET_ERROR, TIMEVAL, + }; let tbl = table.lock().unwrap(); - let mut pollfds: Vec = tbl + let raw_sockets: Vec = tbl .sockets .values() - .map(|hs| WSAPOLLFD { - fd: hs.socket.as_raw_socket() as usize, - // POLLERR is output-only on Windows; setting it in events causes WSAEINVAL - events: (POLLRDNORM | POLLWRNORM) as i16, - revents: 0, - }) + .take(FD_SETSIZE as usize) + .map(|hs| hs.socket.as_raw_socket() as SOCKET) .collect(); drop(tbl); - if pollfds.is_empty() { + if raw_sockets.is_empty() { _sc.wait(Duration::from_nanos(ns)); return Ok(json!({})); } + let mut readfds: FD_SET = unsafe { std::mem::zeroed() }; + let mut writefds: FD_SET = unsafe { std::mem::zeroed() }; + let mut exceptfds: FD_SET = unsafe { std::mem::zeroed() }; + for &s in &raw_sockets { + let rc = readfds.fd_count as usize; + readfds.fd_array[rc] = s; + readfds.fd_count += 1; + let wc = writefds.fd_count as usize; + writefds.fd_array[wc] = s; + writefds.fd_count += 1; + let ec = exceptfds.fd_count as usize; + exceptfds.fd_array[ec] = s; + exceptfds.fd_count += 1; + } + let timeout_ms = ((ns / 1_000_000) as i32).clamp(1, 30_000); - let ret = unsafe { WSAPoll(pollfds.as_mut_ptr(), pollfds.len() as u32, timeout_ms) }; + let tv = TIMEVAL { + tv_sec: timeout_ms / 1000, + tv_usec: (timeout_ms % 1000) * 1000, + }; - if ret < 0 { + let ret = unsafe { select(0, &mut readfds, &mut writefds, &mut exceptfds, &tv) }; + + if ret == SOCKET_ERROR { let err = std::io::Error::last_os_error(); - return Err(anyhow!("hl_sleep WSAPoll failed: {err}")); + return Err(anyhow!("hl_sleep select() failed: {err}")); } - Ok(json!({"socket_ready": ret > 0})) + let ready = ret > 0; + Ok(json!({"socket_ready": ready})) } // ---------------------------------------------------------------------------