Skip to content

Commit 9b57dfc

Browse files
committed
Unwind main() for clean shutdown (vs. libc::_exit())
The design of the existing main() function was such that Firecracker never unwound the stack to the top level in order to end the program. Instead, it would run some Firecracker-specific shutdown code and then call `unsafe { libc::_exit() }`. That skips some Rust-specific shutdown done by std::process::exit(). But even using that does not call destructors for the stack objects, so it is not a "clean shutdown": https://doc.rust-lang.org/std/process/fn.exit.html While a "dirty shutdown" can be a bit faster, the benefit of having non-panic shutdowns run their finalization is that it helps with the accounting of Valgrind and sanitizers. It's a good sanity check to make sure the program could exit cleanly if it wanted to. This commit attempts a somewhat minimally-invasive means of bubbling up an "ExitCode". Exiting was managed uniquely by the Vmm event subscriber during the process() callback, and requires running the vmm.stop() method. So this extends the Subscriber trait with an extended form of callback named process_exitable() that returns an optional exit code. The event loop doesn't have a clear (to me) way to get at the VMM to call stop() if a non-Vmm subscriber requests an exit. So the interface means any other subscriber that overrode process_exitable() would miss the required stop code. So it suggests the need for a redesign by someone more familiar with the architecture. However, it does accomplish a proof of concept by getting closer to a clean bill of health from a Valgrind analysis.
1 parent 7b0f016 commit 9b57dfc

File tree

5 files changed

+116
-39
lines changed

5 files changed

+116
-39
lines changed

src/firecracker/src/api_server_adapter.rs

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ use std::{
1212
use api_server::{ApiRequest, ApiResponse, ApiServer};
1313
use logger::{error, warn, ProcessTimeReporter};
1414
use mmds::MMDS;
15-
use polly::event_manager::{EventManager, Subscriber};
15+
use polly::event_manager::{EventManager, Subscriber, ExitCode};
1616
use seccomp::BpfProgram;
1717
use utils::{
1818
epoll::{EpollEvent, EventSet},
@@ -43,7 +43,7 @@ impl ApiServerAdapter {
4343
vm_resources: VmResources,
4444
vmm: Arc<Mutex<Vmm>>,
4545
event_manager: &mut EventManager,
46-
) {
46+
) -> ExitCode {
4747
let api_adapter = Arc::new(Mutex::new(Self {
4848
api_event_fd,
4949
from_api,
@@ -53,10 +53,13 @@ impl ApiServerAdapter {
5353
event_manager
5454
.add_subscriber(api_adapter)
5555
.expect("Cannot register the api event to the event manager.");
56+
5657
loop {
57-
event_manager
58-
.run()
58+
let opt_exit_code = event_manager
59+
.run_maybe_exiting()
5960
.expect("EventManager events driver fatal error");
61+
62+
if let Some(exit_code) = opt_exit_code { return exit_code; }
6063
}
6164
}
6265

@@ -127,7 +130,7 @@ pub(crate) fn run_with_api(
127130
instance_info: InstanceInfo,
128131
process_time_reporter: ProcessTimeReporter,
129132
boot_timer_enabled: bool,
130-
) {
133+
) -> ExitCode {
131134
// FD to notify of API events. This is a blocking eventfd by design.
132135
// It is used in the config/pre-boot loop which is a simple blocking loop
133136
// which only consumes API events.
@@ -242,5 +245,5 @@ pub(crate) fn run_with_api(
242245
vm_resources,
243246
vmm,
244247
&mut event_manager,
245-
);
248+
)
246249
}

src/firecracker/src/main.rs

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ use std::process;
1111
use std::sync::{Arc, Mutex};
1212

1313
use logger::{error, info, IncMetric, ProcessTimeReporter, LOGGER, METRICS};
14-
use polly::event_manager::EventManager;
14+
use polly::event_manager::{EventManager, ExitCode};
1515
use seccomp::{BpfProgram, SeccompLevel};
1616
use utils::arg_parser::{ArgParser, Argument};
1717
use utils::terminal::Terminal;
@@ -30,7 +30,7 @@ const DEFAULT_API_SOCK_PATH: &str = "/run/firecracker.socket";
3030
const DEFAULT_INSTANCE_ID: &str = "anonymous-instance";
3131
const FIRECRACKER_VERSION: &str = env!("FIRECRACKER_VERSION");
3232

33-
fn main() {
33+
fn main_exitable() -> ExitCode {
3434
LOGGER
3535
.configure(Some(DEFAULT_INSTANCE_ID.to_string()))
3636
.expect("Failed to register logger");
@@ -254,17 +254,32 @@ fn main() {
254254
instance_info,
255255
process_time_reporter,
256256
boot_timer_enabled,
257-
);
257+
)
258258
} else {
259259
run_without_api(
260260
seccomp_filter,
261261
vmm_config_json,
262262
&instance_info,
263263
boot_timer_enabled,
264-
);
264+
)
265265
}
266266
}
267267

268+
fn main () {
269+
// This idiom is the prescribed way to get a clean shutdown of Rust (that will report
270+
// no leaks in Valgrind or sanitizers). Calling `unsafe { libc::exit() }` does no
271+
// cleanup, and std::process::exit() does more--but does not run destructors. So the
272+
// best thing to do is to is bubble up the exit code through the whole stack, and
273+
// only exit when everything potentially destructible has cleaned itself up.
274+
//
275+
// https://doc.rust-lang.org/std/process/fn.exit.html
276+
//
277+
// See process_exitable() method of Subscriber trait for what triggers the exit_code.
278+
//
279+
let exit_code = main_exitable();
280+
std::process::exit(i32::from(exit_code));
281+
}
282+
268283
// Print supported snapshot data format versions.
269284
fn print_supported_snapshot_versions() {
270285
let mut snapshot_versions_str = "Supported snapshot data format versions:".to_string();
@@ -316,7 +331,7 @@ fn run_without_api(
316331
config_json: Option<String>,
317332
instance_info: &InstanceInfo,
318333
bool_timer_enabled: bool,
319-
) {
334+
) -> ExitCode {
320335
let mut event_manager = EventManager::new().expect("Unable to create EventManager");
321336

322337
// Right before creating the signalfd,
@@ -357,8 +372,10 @@ fn run_without_api(
357372

358373
// Run the EventManager that drives everything in the microVM.
359374
loop {
360-
event_manager
361-
.run()
375+
let opt_exit_code = event_manager
376+
.run_maybe_exiting()
362377
.expect("Failed to start the event manager");
378+
379+
if let Some(exit_code) = opt_exit_code { return exit_code; }
363380
}
364381
}

src/polly/src/event_manager.rs

Lines changed: 71 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,15 @@ use utils::epoll::{self, Epoll, EpollEvent};
1212
pub type Result<T> = std::result::Result<T, Error>;
1313
pub type Pollable = RawFd;
1414

15+
/// Process Exit Code, historically an 8 bit number on Unix; use a type alias
16+
/// since `fn something(...) -> u8` is non-obvious.
17+
///
18+
/// https://tldp.org/LDP/abs/html/exitcodes.html
19+
///
20+
/// !!! Should i32 be used instead, since `std::process::exit` allows it?
21+
///
22+
pub type ExitCode = u8;
23+
1524
/// Errors associated with epoll events handling.
1625
pub enum Error {
1726
/// Cannot create epoll fd.
@@ -57,7 +66,25 @@ pub trait Subscriber {
5766
/// The only functions safe to call on this `EventManager` reference
5867
/// are `register`, `unregister` and `modify` which correspond to
5968
/// the `libc::epoll_ctl` operations.
60-
fn process(&mut self, event: &EpollEvent, event_manager: &mut EventManager);
69+
fn process(&mut self, _event: &EpollEvent, _event_manager: &mut EventManager) {
70+
//
71+
// !!! Can it be compile-time checked that one-and-only-one of process() or
72+
// process_exitable() are overloaded?
73+
//
74+
panic!("Either process() or process_exitable() must be implemented for Subscriber");
75+
}
76+
77+
/// Variant of the process() callback that lets the subscriber signal a desire to exit
78+
/// by returning a non-None ExitCode.
79+
///
80+
/// This is what's actually called when dispatching subscriptions, but it delegates
81+
/// to the plain form. That way most subscribers implement process() and can avoid
82+
/// needing to `return None` to signal not exiting.
83+
///
84+
fn process_exitable(&mut self, event: &EpollEvent, event_manager: &mut EventManager) -> Option<ExitCode> {
85+
self.process(event, event_manager);
86+
None
87+
}
6188

6289
/// Returns a list of `EpollEvent` that this subscriber is interested in.
6390
fn interest_list(&self) -> Vec<EpollEvent>;
@@ -170,41 +197,74 @@ impl EventManager {
170197
Ok(())
171198
}
172199

173-
/// Wait for events, then dispatch to the registered event handlers.
174-
pub fn run(&mut self) -> Result<usize> {
175-
self.run_with_timeout(-1)
176-
}
177-
178200
/// Wait for events for a maximum timeout of `miliseconds`. Dispatch the events to the
179201
/// registered signal handlers.
180-
pub fn run_with_timeout(&mut self, milliseconds: i32) -> Result<usize> {
202+
///
203+
/// First return is None if the events were processed and no exit events were encountered.
204+
/// If an exit event happens and `--cleanup-level` is 2, the exit code will be returned.
205+
/// Lower cleanup levels that get exit events will call libc::exit(), thus not return
206+
///
207+
/// Second return is the number of dispatched events, which is only used by the test code.
208+
pub fn run_core(&mut self, milliseconds: i32) -> Result<(Option<ExitCode>, usize)> {
181209
let event_count = match self.epoll.wait(milliseconds, &mut self.ready_events[..]) {
182210
Ok(event_count) => event_count,
183211
Err(e) if e.raw_os_error() == Some(libc::EINTR) => 0,
184212
Err(e) => return Err(Error::Poll(e)),
185213
};
186-
self.dispatch_events(event_count);
214+
let opt_exit_code = self.dispatch_events(event_count);
215+
216+
Ok((opt_exit_code, event_count)) // event count is checked by the tests
217+
}
187218

219+
/// Version of run used by Firecracker, which doesn't need the count of dispatched events
220+
/// or a timeout. It wants to be able to receive an exit code if `--cleanup-level` is 2.
221+
pub fn run_maybe_exiting(&mut self) -> Result<Option<ExitCode>> {
222+
let (opt_exit_code, _event_count) = self.run_core(-1)?;
223+
Ok(opt_exit_code)
224+
}
225+
226+
/// Variation of run used by tests which want a count of dispatched events, and also
227+
/// a timeout. The tests were written before the concept of clean shutdown, so they
228+
/// assume any exit events would have terminated Firecracker abuptly.
229+
pub fn run_with_timeout(&mut self, milliseconds: i32) -> Result<usize> {
230+
let (opt_exit_code, event_count) = self.run_core(milliseconds)?;
231+
assert!(opt_exit_code.is_none());
188232
Ok(event_count)
189233
}
190234

191-
fn dispatch_events(&mut self, event_count: usize) {
235+
/// Variation of run used by tests which don't want to use a timeout.
236+
pub fn run(&mut self) -> Result<usize> {
237+
self.run_with_timeout(-1)
238+
}
239+
240+
fn dispatch_events(&mut self, event_count: usize) -> Option<ExitCode> {
192241
// Use the temporary, pre-allocated buffer to check ready events.
193242
for ev_index in 0..event_count {
194243
let event = &self.ready_events[ev_index].clone();
195244
let pollable = event.fd();
196245

197246
if self.subscribers.contains_key(&pollable) {
198-
self.subscribers
247+
let opt_exit_code = self.subscribers
199248
.get_mut(&pollable)
200249
.unwrap() // Safe because we have already checked existence
201250
.clone()
202251
.lock()
203252
.expect("Poisoned lock")
204-
.process(&event, self);
253+
.process_exitable(&event, self);
254+
255+
if opt_exit_code != None {
256+
//
257+
// Note: allowing other handlers to run could create an
258+
// ambiguity of which exit code to use if more than one asked
259+
// to exit. Thus we signal exit on the first handler that asks.
260+
//
261+
return opt_exit_code;
262+
}
205263
}
206264
// TODO: Should we log an error in case the subscriber does not exist?
207265
}
266+
267+
None
208268
}
209269
}
210270

src/vmm/src/lib.rs

Lines changed: 8 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ use devices::virtio::{
5858
};
5959
use devices::BusDevice;
6060
use logger::{error, info, warn, LoggerError, MetricsError, METRICS};
61-
use polly::event_manager::{EventManager, Subscriber};
61+
use polly::event_manager::{EventManager, Subscriber, ExitCode};
6262
use rate_limiter::BucketUpdate;
6363
use seccomp::BpfProgramRef;
6464
use snapshot::Persist;
@@ -339,8 +339,9 @@ impl Vmm {
339339
.map_err(Error::I8042Error)
340340
}
341341

342-
/// Waits for all vCPUs to exit and terminates the Firecracker process.
343-
pub fn stop(&mut self, exit_code: i32) {
342+
/// Waits for all vCPUs to exit. Does not terminate the Firecracker process.
343+
/// (See notes in main() about why ExitCode is bubbled up for clean shutdown.)
344+
pub fn stop(&mut self) {
344345
info!("Vmm is stopping.");
345346

346347
if let Some(observer) = self.events_observer.as_mut() {
@@ -353,12 +354,6 @@ impl Vmm {
353354
if let Err(e) = METRICS.write() {
354355
error!("Failed to write metrics while stopping: {}", e);
355356
}
356-
357-
// Exit from Firecracker using the provided exit code. Safe because we're terminating
358-
// the process anyway.
359-
unsafe {
360-
libc::_exit(exit_code);
361-
}
362357
}
363358

364359
/// Saves the state of a paused Microvm.
@@ -717,7 +712,7 @@ impl Drop for Vmm {
717712

718713
impl Subscriber for Vmm {
719714
/// Handle a read event (EPOLLIN).
720-
fn process(&mut self, event: &EpollEvent, _: &mut EventManager) {
715+
fn process_exitable(&mut self, event: &EpollEvent, _: &mut EventManager) -> Option<ExitCode> {
721716
let source = event.fd();
722717
let event_set = event.event_set();
723718

@@ -735,9 +730,11 @@ impl Subscriber for Vmm {
735730
_ => None,
736731
})
737732
.unwrap_or(FC_EXIT_CODE_OK);
738-
self.stop(i32::from(exit_code));
733+
self.stop();
734+
Some(exit_code)
739735
} else {
740736
error!("Spurious EventManager event for handler: Vmm");
737+
None
741738
}
742739
}
743740

src/vmm/tests/integration_tests.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ fn test_build_microvm() {
7373
let _ = event_manager.run_with_timeout(500).unwrap();
7474

7575
#[cfg(target_arch = "x86_64")]
76-
vmm.lock().unwrap().stop(-1); // If we got here, something went wrong.
76+
vmm.lock().unwrap().stop(1); // If we got here, something went wrong.
7777
#[cfg(target_arch = "aarch64")]
7878
vmm.lock().unwrap().stop(0);
7979
}
@@ -104,7 +104,7 @@ fn test_vmm_seccomp() {
104104
// Give the vCPUs a chance to attempt KVM_RUN.
105105
thread::sleep(Duration::from_millis(200));
106106
// Should never get here.
107-
vmm.lock().unwrap().stop(-1);
107+
vmm.lock().unwrap().stop(1);
108108
}
109109
vmm_pid => {
110110
// Parent process: wait for the vmm to exit.
@@ -160,7 +160,7 @@ fn test_pause_resume_microvm() {
160160
let _ = event_manager.run_with_timeout(500).unwrap();
161161

162162
#[cfg(target_arch = "x86_64")]
163-
vmm.lock().unwrap().stop(-1); // If we got here, something went wrong.
163+
vmm.lock().unwrap().stop(1); // If we got here, something went wrong.
164164
#[cfg(target_arch = "aarch64")]
165165
vmm.lock().unwrap().stop(0);
166166
}
@@ -193,7 +193,7 @@ fn test_dirty_bitmap_error() {
193193
let _ = event_manager.run_with_timeout(500).unwrap();
194194

195195
#[cfg(target_arch = "x86_64")]
196-
vmm.lock().unwrap().stop(-1); // If we got here, something went wrong.
196+
vmm.lock().unwrap().stop(1); // If we got here, something went wrong.
197197
#[cfg(target_arch = "aarch64")]
198198
vmm.lock().unwrap().stop(0);
199199
}

0 commit comments

Comments
 (0)