Skip to main content

hyperv_ic_guest/
shutdown.rs

1// Copyright (c) Microsoft Corporation.
2// Licensed under the MIT License.
3
4//! The shutdown IC client.
5
6#![forbid(unsafe_code)]
7
8pub use hyperv_ic_protocol::shutdown::INSTANCE_ID;
9pub use hyperv_ic_protocol::shutdown::INTERFACE_ID;
10
11#[cfg(test)]
12#[path = "shutdown_tests.rs"]
13mod tests;
14
15use guid::Guid;
16use hyperv_ic_protocol::FRAMEWORK_VERSION_1;
17use hyperv_ic_protocol::FRAMEWORK_VERSION_3;
18use hyperv_ic_protocol::Status;
19use hyperv_ic_protocol::shutdown::SHUTDOWN_VERSION_1;
20use hyperv_ic_protocol::shutdown::SHUTDOWN_VERSION_3;
21use hyperv_ic_protocol::shutdown::SHUTDOWN_VERSION_3_1;
22use hyperv_ic_protocol::shutdown::SHUTDOWN_VERSION_3_2;
23use hyperv_ic_resources::shutdown::ShutdownParams;
24use hyperv_ic_resources::shutdown::ShutdownResult;
25use hyperv_ic_resources::shutdown::ShutdownType;
26use inspect::Inspect;
27use inspect::InspectMut;
28use mesh::rpc::Rpc;
29use mesh::rpc::RpcSend;
30use std::io::IoSlice;
31use std::mem::size_of_val;
32use task_control::Cancelled;
33use task_control::StopTask;
34use thiserror::Error;
35use vmbus_async::async_dgram::AsyncRecvExt;
36use vmbus_async::async_dgram::AsyncSendExt;
37use vmbus_async::pipe::MessagePipe;
38use vmbus_channel::RawAsyncChannel;
39use vmbus_channel::channel::ChannelOpenError;
40use vmbus_relay_intercept_device::OfferResponse;
41use vmbus_relay_intercept_device::SaveRestoreSimpleVmbusClientDevice;
42use vmbus_relay_intercept_device::SimpleVmbusClientDevice;
43use vmbus_relay_intercept_device::SimpleVmbusClientDeviceAsync;
44use vmbus_relay_intercept_device::ring_buffer::MemoryBlockRingBuffer;
45use vmbus_ring::RingMem;
46use vmcore::save_restore::NoSavedState;
47use zerocopy::FromBytes;
48use zerocopy::FromZeros;
49use zerocopy::IntoBytes;
50
51/// A shutdown IC client device.
52#[derive(InspectMut)]
53pub struct ShutdownGuestIc {
54    #[inspect(skip)]
55    send_shutdown_notification: mesh::Sender<Rpc<ShutdownParams, ShutdownResult>>,
56    #[inspect(skip)]
57    recv_shutdown_notification: Option<mesh::Receiver<Rpc<ShutdownParams, ShutdownResult>>>,
58}
59
60#[derive(Debug, Inspect)]
61#[inspect(tag = "channel_state")]
62enum ShutdownGuestChannelState {
63    NegotiateVersion,
64    Running {
65        #[inspect(display)]
66        framework_version: hyperv_ic_protocol::Version,
67        #[inspect(display)]
68        message_version: hyperv_ic_protocol::Version,
69    },
70}
71
72/// Established channel between guest and host.
73#[derive(InspectMut)]
74pub struct ShutdownGuestChannel<M: RingMem = MemoryBlockRingBuffer> {
75    /// Current state.
76    state: ShutdownGuestChannelState,
77    /// Vmbus pipe to the host.
78    #[inspect(mut)]
79    pipe: MessagePipe<M>,
80}
81
82#[derive(Debug, Error)]
83enum Error {
84    #[error("cancelled")]
85    Cancelled(Cancelled),
86    #[error("ring buffer error")]
87    Ring(#[source] std::io::Error),
88    #[error("truncated message")]
89    TruncatedMessage,
90}
91
92impl From<Cancelled> for Error {
93    fn from(value: Cancelled) -> Self {
94        Self::Cancelled(value)
95    }
96}
97
98impl ShutdownGuestIc {
99    /// Returns a new shutdown IC client device.
100    pub fn new() -> Self {
101        let (send_shutdown_notification, recv_shutdown_notification) = mesh::channel();
102        Self {
103            send_shutdown_notification,
104            recv_shutdown_notification: Some(recv_shutdown_notification),
105        }
106    }
107
108    /// Returns the notifier that will receive any shutdown requests from the host.
109    pub fn get_shutdown_notifier(&mut self) -> mesh::Receiver<Rpc<ShutdownParams, ShutdownResult>> {
110        self.recv_shutdown_notification
111            .take()
112            .expect("can only be called once")
113    }
114}
115
116impl<M: RingMem + Unpin> ShutdownGuestChannel<M> {
117    fn new(pipe: MessagePipe<M>) -> Self {
118        Self {
119            state: ShutdownGuestChannelState::NegotiateVersion,
120            pipe,
121        }
122    }
123
124    async fn process(
125        &mut self,
126        ic: &mut ShutdownGuestIc,
127        check: &task_control::StopCheck<'_>,
128    ) -> Result<(), Error> {
129        loop {
130            check.check()?;
131            match read_from_pipe(&mut self.pipe).await {
132                Ok(buf) => {
133                    self.handle_host_message(&buf, ic).await;
134                }
135                Err(err) => {
136                    tracelimit::error_ratelimited!(
137                        err = &err as &dyn std::error::Error,
138                        "reading shutdown packet from host",
139                    );
140                }
141            }
142        }
143    }
144
145    async fn handle_host_message(&mut self, buf: &[u8], ic: &ShutdownGuestIc) {
146        // TODO: zerocopy: err (https://github.com/microsoft/openvmm/issues/759)
147        let (header, rest) = match hyperv_ic_protocol::Header::read_from_prefix(buf).ok() {
148            Some((h, r)) => (h, r),
149            None => {
150                tracelimit::error_ratelimited!("invalid shutdown packet from host",);
151                return;
152            }
153        };
154        match header.message_type {
155            hyperv_ic_protocol::MessageType::VERSION_NEGOTIATION => {
156                // Version negotiation can happen multiple times due to various
157                // state changes on the host. This message triggers a reset
158                // of the current state.
159                self.state = ShutdownGuestChannelState::NegotiateVersion;
160                if let Err(err) = self.handle_version_negotiation(&header, rest).await {
161                    tracelimit::error_ratelimited!(
162                        err = &err as &dyn std::error::Error,
163                        "Failed version negotiation"
164                    );
165                }
166            }
167            hyperv_ic_protocol::MessageType::SHUTDOWN
168                if matches!(self.state, ShutdownGuestChannelState::Running { .. }) =>
169            {
170                if let Err(err) = self.handle_shutdown_notification(&header, rest, ic).await {
171                    tracelimit::error_ratelimited!(
172                        err = &err as &dyn std::error::Error,
173                        "Failed processing shutdown message"
174                    );
175                }
176            }
177            _ => {
178                tracelimit::error_ratelimited!(r#type = ?header.message_type, state = ?self.state, "Unrecognized packet");
179            }
180        }
181    }
182
183    fn find_latest_supported_version<'a>(
184        buf: &'a [u8],
185        count: usize,
186        supported: &[hyperv_ic_protocol::Version],
187    ) -> (Option<hyperv_ic_protocol::Version>, &'a [u8]) {
188        let mut rest = buf;
189        let mut next_version;
190        let mut latest_version = None;
191        for _ in 0..count {
192            // TODO: zerocopy: err (https://github.com/microsoft/openvmm/issues/759)
193            (next_version, rest) = match hyperv_ic_protocol::Version::read_from_prefix(rest).ok() {
194                Some((n, r)) => (n, r),
195                None => {
196                    tracelimit::error_ratelimited!("truncated message version list");
197                    return (latest_version, rest);
198                }
199            };
200            for known in supported {
201                if known.major == next_version.major && known.minor == next_version.minor {
202                    if latest_version.is_some() {
203                        if next_version.major >= latest_version.unwrap().major {
204                            if next_version.major > latest_version.unwrap().major
205                                || next_version.minor > latest_version.unwrap().minor
206                            {
207                                latest_version = Some(next_version);
208                            }
209                        }
210                    } else {
211                        latest_version = Some(next_version);
212                    }
213                }
214            }
215        }
216        (latest_version, rest)
217    }
218
219    async fn handle_version_negotiation(
220        &mut self,
221        header: &hyperv_ic_protocol::Header,
222        msg: &[u8],
223    ) -> Result<(), Error> {
224        const FRAMEWORK_VERSIONS: &[hyperv_ic_protocol::Version] =
225            &[FRAMEWORK_VERSION_1, FRAMEWORK_VERSION_3];
226
227        const SHUTDOWN_VERSIONS: &[hyperv_ic_protocol::Version] = &[
228            SHUTDOWN_VERSION_1,
229            SHUTDOWN_VERSION_3,
230            SHUTDOWN_VERSION_3_1,
231            SHUTDOWN_VERSION_3_2,
232        ];
233
234        let (prefix, rest) = hyperv_ic_protocol::NegotiateMessage::read_from_prefix(msg)
235            .map_err(|_| Error::TruncatedMessage)?; // TODO: zerocopy: map_err (https://github.com/microsoft/openvmm/issues/759)
236        let (latest_framework_version, rest) = Self::find_latest_supported_version(
237            rest,
238            prefix.framework_version_count as usize,
239            FRAMEWORK_VERSIONS,
240        );
241        let framework_version = if let Some(version) = latest_framework_version {
242            version
243        } else {
244            tracelimit::error_ratelimited!("Unsupported framework version");
245            FRAMEWORK_VERSIONS[FRAMEWORK_VERSIONS.len() - 1]
246        };
247        let (latest_message_version, _) = Self::find_latest_supported_version(
248            rest,
249            prefix.message_version_count as usize,
250            SHUTDOWN_VERSIONS,
251        );
252        let message_version = if let Some(version) = latest_message_version {
253            version
254        } else {
255            tracelimit::error_ratelimited!("Unsupported message version");
256            SHUTDOWN_VERSIONS[SHUTDOWN_VERSIONS.len() - 1]
257        };
258
259        let message = hyperv_ic_protocol::NegotiateMessage {
260            framework_version_count: 1,
261            message_version_count: 1,
262            ..FromZeros::new_zeroed()
263        };
264        let response = hyperv_ic_protocol::Header {
265            message_type: hyperv_ic_protocol::MessageType::VERSION_NEGOTIATION,
266            message_size: (size_of_val(&message)
267                + size_of_val(&framework_version)
268                + size_of_val(&message_version)) as u16,
269            status: Status::SUCCESS,
270            transaction_id: header.transaction_id,
271            flags: hyperv_ic_protocol::HeaderFlags::new()
272                .with_transaction(header.flags.transaction())
273                .with_response(true),
274            ..FromZeros::new_zeroed()
275        };
276        self.pipe
277            .send_vectored(&[
278                IoSlice::new(response.as_bytes()),
279                IoSlice::new(message.as_bytes()),
280                IoSlice::new(framework_version.as_bytes()),
281                IoSlice::new(message_version.as_bytes()),
282            ])
283            .await
284            .map_err(Error::Ring)?;
285
286        tracing::info!(%framework_version, %message_version, "version negotiated");
287        self.state = ShutdownGuestChannelState::Running {
288            framework_version,
289            message_version,
290        };
291        Ok(())
292    }
293
294    async fn handle_shutdown_notification(
295        &mut self,
296        header: &hyperv_ic_protocol::Header,
297        buf: &[u8],
298        ic: &ShutdownGuestIc,
299    ) -> Result<(), Error> {
300        let ShutdownGuestChannelState::Running {
301            framework_version,
302            message_version,
303        } = &self.state
304        else {
305            panic!("Shutdown message processing while in invalid state");
306        };
307
308        let message = hyperv_ic_protocol::shutdown::ShutdownMessage::read_from_prefix(buf)
309            .map_err(|_| Error::TruncatedMessage)?
310            .0; // TODO: zerocopy: map_err (https://github.com/microsoft/openvmm/issues/759)
311        let shutdown_type = if message.flags.restart() {
312            ShutdownType::Reboot
313        } else if message.flags.hibernate() {
314            ShutdownType::Hibernate
315        } else {
316            ShutdownType::PowerOff
317        };
318        let params = ShutdownParams {
319            shutdown_type,
320            force: message.flags.force(),
321        };
322
323        // Notify the internal listener and wait for a response.
324        let status = match ic.send_shutdown_notification.call(|x| x, params).await {
325            Ok(ShutdownResult::Ok) => Status::SUCCESS,
326            Ok(ShutdownResult::Failed(x)) => Status(x),
327            Ok(ShutdownResult::NotReady) | Ok(ShutdownResult::AlreadyInProgress) | Err(_) => {
328                Status::FAIL
329            }
330        };
331
332        // Respond to the request.
333        let response = hyperv_ic_protocol::Header {
334            framework_version: *framework_version,
335            message_version: *message_version,
336            message_type: hyperv_ic_protocol::MessageType::SHUTDOWN,
337            message_size: 0,
338            status,
339            transaction_id: header.transaction_id,
340            flags: hyperv_ic_protocol::HeaderFlags::new()
341                .with_transaction(header.flags.transaction())
342                .with_response(true),
343            ..FromZeros::new_zeroed()
344        };
345        self.pipe
346            .send(response.as_bytes())
347            .await
348            .map_err(Error::Ring)
349    }
350}
351
352async fn read_from_pipe<T: RingMem>(pipe: &mut MessagePipe<T>) -> Result<Vec<u8>, Error> {
353    let mut buf = vec![0; hyperv_ic_protocol::MAX_MESSAGE_SIZE];
354    let n = pipe.recv(&mut buf).await.map_err(Error::Ring)?;
355    let buf = &buf[..n];
356    Ok(buf.to_vec())
357}
358
359impl SimpleVmbusClientDevice for ShutdownGuestIc {
360    type SavedState = NoSavedState;
361    type Runner = ShutdownGuestChannel;
362
363    fn instance_id(&self) -> Guid {
364        INSTANCE_ID
365    }
366
367    fn offer(&self, _offer: &vmbus_core::protocol::OfferChannel) -> OfferResponse {
368        OfferResponse::Open
369    }
370
371    fn inspect(&mut self, req: inspect::Request<'_>, runner: Option<&mut Self::Runner>) {
372        req.respond().merge(self).merge(runner);
373    }
374
375    fn open(
376        &mut self,
377        _channel_idx: u16,
378        channel: RawAsyncChannel<MemoryBlockRingBuffer>,
379    ) -> Result<Self::Runner, ChannelOpenError> {
380        let pipe = MessagePipe::new(channel)?;
381        Ok(ShutdownGuestChannel::new(pipe))
382    }
383
384    fn close(&mut self, _channel_idx: u16) {}
385
386    fn supports_save_restore(
387        &mut self,
388    ) -> Option<
389        &mut dyn SaveRestoreSimpleVmbusClientDevice<
390            SavedState = Self::SavedState,
391            Runner = Self::Runner,
392        >,
393    > {
394        None
395    }
396}
397
398impl SimpleVmbusClientDeviceAsync for ShutdownGuestIc {
399    async fn run(
400        &mut self,
401        stop: &mut StopTask<'_>,
402        runner: &mut Self::Runner,
403    ) -> Result<(), Cancelled> {
404        let check = stop.check_handle()?;
405        stop.until_stopped(async {
406            match runner.process(self, &check).await {
407                Ok(()) => {}
408                Err(Error::Cancelled(cancelled)) => return Err(cancelled),
409                Err(err) => {
410                    tracing::error!(
411                        error = &err as &dyn std::error::Error,
412                        "shutdown ic relay error"
413                    )
414                }
415            }
416            Ok(())
417        })
418        .await?
419    }
420}