Skip to content

Commit 22f0641

Browse files
committed
feat: Added uhyve snapshot and restore functionality
1 parent f7c08dd commit 22f0641

4 files changed

Lines changed: 181 additions & 11 deletions

File tree

Cargo.toml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,12 @@ loader = ["hermit-entry"]
9595
## [Uhyve]: https://github.com/hermit-os/uhyve
9696
uhyve = ["hermit-entry", "dep:uhyve-interface"]
9797

98+
## Enables taking and restoring [Uhyve's](Uhyve) VM snapshots.
99+
##
100+
## This feature is incompatible with `write-pcap-file`.
101+
## [Uhyve]: https://github.com/hermit-os/uhyve
102+
snapshot = ["uhyve"]
103+
98104
#! ### Hardware Features
99105
#!
100106
#! [microvm]: https://www.qemu.org/docs/master/system/i386/microvm.html

src/executor/network.rs

Lines changed: 121 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ use alloc::boxed::Box;
22
#[cfg(feature = "dns")]
33
use alloc::vec::Vec;
44
use core::future;
5-
use core::sync::atomic::{AtomicU16, Ordering};
5+
use core::sync::atomic::{AtomicBool, AtomicU16, Ordering};
66
use core::task::Poll;
77

88
use hermit_sync::InterruptTicketMutex;
@@ -79,6 +79,8 @@ static LOCAL_ENDPOINT: AtomicU16 = AtomicU16::new(0);
7979
pub(crate) static NIC: InterruptTicketMutex<NetworkState<'_>> =
8080
InterruptTicketMutex::new(NetworkState::Missing);
8181

82+
static NETWORK_POLLER_STARTED: AtomicBool = AtomicBool::new(false);
83+
8284
type MaybePcapDevice = cfg_select! {
8385
feature = "write-pcap-file" => {
8486
smoltcp::phy::PcapWriter<NetworkDevice, crate::executor::device::pcap_writer::FileSink>
@@ -138,17 +140,26 @@ pub(crate) fn now() -> Instant {
138140
Instant::from_micros_const(systemtime::now_micros().try_into().unwrap())
139141
}
140142

143+
/// Whether DHCP has configured the *current* network stack.
144+
///
145+
/// A new DHCP socket reports [`dhcpv4::Event::Deconfigured`] on its first poll,
146+
/// which must not tear down the configuration the stack was created with.
147+
#[cfg(feature = "dhcpv4")]
148+
static DHCP_WAS_EVER_CONFIGURED: AtomicBool = AtomicBool::new(false);
149+
141150
#[cfg(feature = "dhcpv4")]
142151
async fn dhcpv4_run() {
143-
let mut was_ever_configured = false;
144152
future::poll_fn(|cx| {
145153
let Some(mut guard) = NIC.try_lock() else {
146154
// FIXME: only wake when progress can be made
147155
cx.waker().wake_by_ref();
148156
return Poll::Pending;
149157
};
150158

151-
let nic = guard.as_nic_mut().unwrap();
159+
let Ok(nic) = guard.as_nic_mut() else {
160+
NETWORK_WAKER.lock().register(cx.waker());
161+
return Poll::Pending;
162+
};
152163
let dhcp_handle = nic.dhcp_handle;
153164
let socket = nic.sockets.get_mut::<dhcpv4::Socket<'_>>(dhcp_handle);
154165

@@ -192,10 +203,10 @@ async fn dhcpv4_run() {
192203
nic.dns_handle = Some(nic.sockets.add(dns_socket));
193204
}
194205

195-
was_ever_configured = true;
206+
DHCP_WAS_EVER_CONFIGURED.store(true, Ordering::Relaxed);
196207
}
197208
Some(dhcpv4::Event::Deconfigured) => {
198-
if !was_ever_configured {
209+
if !DHCP_WAS_EVER_CONFIGURED.load(Ordering::Relaxed) {
199210
// If there is a default configuration, we do not want to reset it. If there is not, there is not a need to reset it.
200211
return Poll::Pending;
201212
}
@@ -244,7 +255,7 @@ pub(crate) fn wake_network_waker() {
244255
}
245256

246257
async fn network_run() {
247-
future::poll_fn(|cx| {
258+
future::poll_fn(|cx| -> Poll<()> {
248259
let Some(mut guard) = NIC.try_lock() else {
249260
// FIXME: only wake when progress can be made
250261
cx.waker().wake_by_ref();
@@ -253,7 +264,8 @@ async fn network_run() {
253264
};
254265

255266
let NetworkState::Initialized(nic) = &mut *guard else {
256-
return Poll::Ready(());
267+
NETWORK_WAKER.lock().register(cx.waker());
268+
return Poll::Pending;
257269
};
258270

259271
let now = now();
@@ -285,7 +297,10 @@ pub(crate) async fn get_query_result(query: QueryHandle) -> io::Result<Vec<IpAdd
285297
return Poll::Pending;
286298
};
287299

288-
let nic = guard.as_nic_mut().unwrap();
300+
let Ok(nic) = guard.as_nic_mut() else {
301+
NETWORK_WAKER.lock().register(cx.waker());
302+
return Poll::Pending;
303+
};
289304
let socket = nic.get_mut_dns_socket()?;
290305
match socket.get_query_result(query) {
291306
Ok(addrs) => {
@@ -309,6 +324,103 @@ pub(crate) async fn get_query_result(query: QueryHandle) -> io::Result<Vec<IpAdd
309324
.await
310325
}
311326

327+
fn spawn_network_executor_tasks_once() {
328+
if NETWORK_POLLER_STARTED
329+
.compare_exchange(false, true, Ordering::SeqCst, Ordering::SeqCst)
330+
.is_ok()
331+
{
332+
spawn(network_run());
333+
#[cfg(feature = "dhcpv4")]
334+
spawn(dhcpv4_run());
335+
}
336+
}
337+
338+
/// Fully tears down the current [`NIC`] state, returns the hardware driver to
339+
/// [`super::device::NETWORK_DEVICE`] when applicable, and rebuilds the stack
340+
/// the same way as [`init`].
341+
///
342+
/// Handles of sockets that were open before the call do not refer to a valid
343+
/// socket afterwards.
344+
#[cfg(feature = "snapshot")]
345+
pub(crate) fn reinit() {
346+
LOCAL_ENDPOINT.store(start_endpoint(), Ordering::Relaxed);
347+
#[cfg(feature = "dhcpv4")]
348+
DHCP_WAS_EVER_CONFIGURED.store(false, Ordering::Relaxed);
349+
350+
let mut guard = NIC.lock();
351+
let old = core::mem::replace(&mut *guard, NetworkState::Missing);
352+
353+
if let NetworkState::Initialized(nic_box) = old {
354+
cfg_select! {
355+
any(
356+
all(target_arch = "riscv64", feature = "gem-net", not(feature = "pci")),
357+
feature = "rtl8139",
358+
feature = "virtio-net",
359+
) => {
360+
let dev = recycle_hardware_device(*nic_box);
361+
*super::device::NETWORK_DEVICE.lock() = Some(dev);
362+
}
363+
_ => {
364+
drop(nic_box);
365+
}
366+
}
367+
}
368+
369+
*guard = NetworkInterface::create();
370+
371+
if let NetworkState::Initialized(_) = &*guard {
372+
spawn_network_executor_tasks_once();
373+
}
374+
375+
drop(guard);
376+
wake_network_waker();
377+
}
378+
379+
#[cfg(all(
380+
feature = "snapshot",
381+
any(
382+
all(target_arch = "riscv64", feature = "gem-net", not(feature = "pci")),
383+
feature = "rtl8139",
384+
feature = "virtio-net",
385+
),
386+
))]
387+
fn recycle_hardware_device(nic: NetworkInterface<'_>) -> NetworkDevice {
388+
use smoltcp::phy::{Device, RxToken};
389+
390+
/// Upper bound for the packets dropped by a single [`recycle_hardware_device`] call.
391+
const DRAIN_LIMIT: usize = 256;
392+
393+
let NetworkInterface {
394+
iface,
395+
sockets,
396+
device,
397+
..
398+
} = nic;
399+
drop((iface, sockets));
400+
let mut device = {
401+
#[cfg(feature = "net-trace")]
402+
let device = device.into_inner();
403+
device
404+
};
405+
406+
// The sockets these packets belong to are gone, so drop what the device
407+
// still holds instead of feeding it to the new interface.
408+
let now = now();
409+
let mut dropped = 0;
410+
while dropped < DRAIN_LIMIT {
411+
let Some((rx, _tx)) = device.receive(now) else {
412+
break;
413+
};
414+
rx.consume(|_| ());
415+
dropped += 1;
416+
}
417+
if dropped > 0 {
418+
debug!("Dropped {dropped} packets that were received before the reinit");
419+
}
420+
421+
device
422+
}
423+
312424
pub(crate) fn init() {
313425
info!("Try to initialize network!");
314426

@@ -320,9 +432,7 @@ pub(crate) fn init() {
320432
*guard = NetworkInterface::create();
321433

322434
if let NetworkState::Initialized(_) = &mut *guard {
323-
spawn(network_run());
324-
#[cfg(feature = "dhcpv4")]
325-
spawn(dhcpv4_run());
435+
spawn_network_executor_tasks_once();
326436
}
327437
}
328438

src/lib.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,9 @@
7878
#![feature(strict_provenance_lints)]
7979
#![warn(implicit_provenance_casts)]
8080

81+
#[cfg(all(feature = "snapshot", feature = "write-pcap-file", not(doc)))]
82+
compile_error!("The `snapshot` feature is incompatible with the `write-pcap-file` feature.");
83+
8184
// EXTERNAL CRATES
8285
#[macro_use]
8386
extern crate alloc;

src/syscalls/system.rs

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,3 +32,54 @@ pub unsafe extern "C" fn sys_read_keyboard(buffer: *mut u8, size: usize, nonbloc
3232
result as isize
3333
}
3434
}
35+
/// Returns the number of bytes written to the resume_params buffer.
36+
#[hermit_macro::system]
37+
#[unsafe(no_mangle)]
38+
#[cfg(all(feature = "snapshot", feature = "uhyve"))]
39+
pub unsafe extern "C" fn sys_snapshot(resume_params: *mut u8, len: u64) -> u64 {
40+
use core::ffi::CStr;
41+
#[cfg(feature = "net")]
42+
use core::str::FromStr;
43+
44+
use uhyve_interface::GuestPhysAddr;
45+
use uhyve_interface::v2::Hypercall;
46+
use uhyve_interface::v2::parameters::SnapshotParams;
47+
48+
use crate::alloc::string::ToString;
49+
use crate::env::{self, UhyveStartInfo, insert_var};
50+
#[cfg(feature = "net")]
51+
use crate::executor::network;
52+
use crate::uhyve::uhyve_hypercall;
53+
54+
assert!(env::start_info().is_uhyve());
55+
let new_args = if resume_params.is_null() {
56+
GuestPhysAddr::zero()
57+
} else {
58+
GuestPhysAddr::new(
59+
crate::arch::mm::paging::virtual_to_physical(crate::mm::VirtAddr::from_ptr(
60+
resume_params,
61+
))
62+
.unwrap()
63+
.as_u64(),
64+
)
65+
};
66+
let mut snapshot_params = SnapshotParams {
67+
new_args,
68+
new_args_len: len,
69+
..Default::default()
70+
};
71+
uhyve_hypercall(Hypercall::Snapshot(&mut snapshot_params));
72+
73+
if snapshot_params.restored {
74+
if let Some(ip) = snapshot_params.new_hermit_ip {
75+
let ip = CStr::from_bytes_until_nul(&ip)
76+
.expect("The hypervisor supplied an unterminated HERMIT_IP")
77+
.to_str()
78+
.expect("The hypervisor supplied a non-UTF-8 HERMIT_IP");
79+
insert_var("HERMIT_IP", ip.to_string());
80+
};
81+
#[cfg(feature = "net")]
82+
network::reinit();
83+
}
84+
snapshot_params.new_args_len
85+
}

0 commit comments

Comments
 (0)