Skip to content

Commit f7b7813

Browse files
committed
metrics: fix race condition in signal handler
The METRICS.write() method operates on the false assumption that it is only being called from one thread (VMM). This is not true since it is called from the signal handlers as well (which may be executed on any thread). The race condition appears due to the fact that metrics state is mutated during serialization for SharedIncMetrics (old value gets assigned the new value after the difference is serialized). The METRICS.write() method only holds the lock when writing to the file, but there is no lock held while serialising the metrics to JSON. VMM THREAD (1/MINUTE) VCPU THREAD (SIGNAL HANDLER) new_val=1 dif=new_val-old_val(=1) old_val=new_val (=1) dif=new_val-old_val (=0) write(dif) (=0) exit() Because of the exit(), the VMM thread no longer gets a chance to write the metric. We now use SharedStoreMetric for deadly signal metrics, which does not have this type of race condition because it is not mutated on serialization. It also does not make sense to have a SharedIncMetric for deadly signal metrics, which can only ever be 0 or 1. This was the root-cause of the intermittent failure in test_custom_seccomp.py::test_failing_fiter Signed-off-by: alindima <[email protected]>
1 parent eb8de3b commit f7b7813

File tree

6 files changed

+41
-31
lines changed

6 files changed

+41
-31
lines changed

src/firecracker/src/main.rs

Lines changed: 2 additions & 2 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 event_manager::SubscriberOps;
14-
use logger::{error, info, IncMetric, ProcessTimeReporter, LOGGER, METRICS};
14+
use logger::{error, info, ProcessTimeReporter, StoreMetric, LOGGER, METRICS};
1515
use mmds::MMDS;
1616
use seccompiler::BpfThreadMap;
1717
use snapshot::Snapshot;
@@ -96,7 +96,7 @@ fn main_exitable() -> ExitCode {
9696
);
9797
}
9898

99-
METRICS.vmm.panic_count.inc();
99+
METRICS.vmm.panic_count.store(1);
100100

101101
// Write the metrics before aborting.
102102
if let Err(e) = METRICS.write() {

src/firecracker/src/metrics.rs

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,8 +46,6 @@ impl PeriodicMetrics {
4646
}
4747

4848
fn write_metrics(&mut self) {
49-
// Please note that, if METRICS has no output file configured yet, it will write to
50-
// stdout, so metrics writing will interfere with console output.
5149
if let Err(e) = METRICS.write() {
5250
METRICS.logger.missed_metrics_count.inc();
5351
error!("Failed to write metrics: {}", e);

src/logger/src/metrics.rs

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -134,10 +134,19 @@ impl<T: Serialize> Metrics<T> {
134134
/// Upon success, the function will return `True` (if metrics system was initialized and metrics
135135
/// were successfully written to disk) or `False` (if metrics system was not yet initialized).
136136
///
137-
/// This function is supposed to be called only from a single thread and
137+
/// This function is usually supposed to be called only from a single thread and
138138
/// is not meant to be used in a multithreaded scenario. The reason
139139
/// `metrics_buf` is enclosed in a `Mutex` is that `lazy_static` enforces
140140
/// thread-safety on all its members.
141+
/// The only exception is for signal handlers that result in process exit, which may be run on
142+
/// any thread. To prevent the race condition present in the serialisation step of
143+
/// SharedIncMetrics, deadly signals use SharedStoreMetrics instead (which have a thread-safe
144+
/// serialise implementation).
145+
/// The only known caveat is that other metrics may not be properly written before exiting from
146+
/// a signal handler. We make this compromise since the process will be killed anyway and the
147+
/// important metric in this case is the signal one.
148+
/// The alternative is to hold a Mutex over the entire function call, but this increases the
149+
/// known deadlock potential.
141150
pub fn write(&self) -> Result<bool, MetricsError> {
142151
if self.is_initialized.load(Ordering::Relaxed) {
143152
match serde_json::to_string(&self.app_metrics) {
@@ -674,7 +683,7 @@ impl RtcEvents for RTCDeviceMetrics {
674683
#[derive(Default, Serialize)]
675684
pub struct SeccompMetrics {
676685
/// Number of errors inside the seccomp filtering.
677-
pub num_faults: SharedIncMetric,
686+
pub num_faults: SharedStoreMetric,
678687
}
679688

680689
/// Metrics specific to the UART device.
@@ -695,22 +704,25 @@ pub struct SerialDeviceMetrics {
695704
}
696705

697706
/// Metrics related to signals.
707+
/// Deadly signals must be of `SharedStoreMetric` type, since they can ever be either 0 or 1.
708+
/// This avoids a tricky race condition caused by the unatomic serialize method of
709+
/// `SharedIncMetric`, between two threads calling `METRICS.write()`.
698710
#[derive(Default, Serialize)]
699711
pub struct SignalMetrics {
700712
/// Number of times that SIGBUS was handled.
701-
pub sigbus: SharedIncMetric,
713+
pub sigbus: SharedStoreMetric,
702714
/// Number of times that SIGSEGV was handled.
703-
pub sigsegv: SharedIncMetric,
715+
pub sigsegv: SharedStoreMetric,
704716
/// Number of times that SIGXFSZ was handled.
705-
pub sigxfsz: SharedIncMetric,
717+
pub sigxfsz: SharedStoreMetric,
706718
/// Number of times that SIGXCPU was handled.
707-
pub sigxcpu: SharedIncMetric,
719+
pub sigxcpu: SharedStoreMetric,
708720
/// Number of times that SIGPIPE was handled.
709721
pub sigpipe: SharedIncMetric,
710722
/// Number of times that SIGHUP was handled.
711-
pub sighup: SharedIncMetric,
723+
pub sighup: SharedStoreMetric,
712724
/// Number of times that SIGILL was handled.
713-
pub sigill: SharedIncMetric,
725+
pub sigill: SharedStoreMetric,
714726
}
715727

716728
/// Metrics specific to VCPUs' mode of functioning.
@@ -736,7 +748,7 @@ pub struct VmmMetrics {
736748
/// Number of device related events received for a VM.
737749
pub device_events: SharedIncMetric,
738750
/// Metric for signaling a panic has occurred.
739-
pub panic_count: SharedIncMetric,
751+
pub panic_count: SharedStoreMetric,
740752
}
741753

742754
/// Vsock-related metrics.

src/vmm/src/signal_handler.rs

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ use libc::{
66
};
77

88
use crate::{ExitCode, FC_EXIT_CODE_UNEXPECTED_ERROR};
9-
use logger::{error, IncMetric, METRICS};
9+
use logger::{error, IncMetric, StoreMetric, METRICS};
1010
use utils::signal::register_signal_handler;
1111

1212
// The offset of `si_syscall` (offending syscall identifier) within the siginfo structure
@@ -39,7 +39,7 @@ macro_rules! generate_handler {
3939
if num != si_signo || num != $signal_name {
4040
exit_with_code(FC_EXIT_CODE_UNEXPECTED_ERROR);
4141
}
42-
$signal_metric.inc();
42+
$signal_metric.store(1);
4343

4444
error!(
4545
"Shutting down VM after intercepting signal {}, code {}.",
@@ -209,31 +209,31 @@ mod tests {
209209
let filter = make_test_seccomp_bpf_filter();
210210

211211
assert!(seccompiler::apply_filter(&filter).is_ok());
212-
assert_eq!(METRICS.seccomp.num_faults.count(), 0);
212+
assert_eq!(METRICS.seccomp.num_faults.fetch(), 0);
213213

214214
// Call the forbidden `SYS_mkdirat`.
215215
unsafe { libc::syscall(libc::SYS_mkdirat, "/foo/bar\0") };
216216

217217
// Call SIGBUS signal handler.
218-
assert_eq!(METRICS.signals.sigbus.count(), 0);
218+
assert_eq!(METRICS.signals.sigbus.fetch(), 0);
219219
unsafe {
220220
syscall(libc::SYS_kill, process::id(), SIGBUS);
221221
}
222222

223223
// Call SIGSEGV signal handler.
224-
assert_eq!(METRICS.signals.sigsegv.count(), 0);
224+
assert_eq!(METRICS.signals.sigsegv.fetch(), 0);
225225
unsafe {
226226
syscall(libc::SYS_kill, process::id(), SIGSEGV);
227227
}
228228

229229
// Call SIGXFSZ signal handler.
230-
assert_eq!(METRICS.signals.sigxfsz.count(), 0);
230+
assert_eq!(METRICS.signals.sigxfsz.fetch(), 0);
231231
unsafe {
232232
syscall(libc::SYS_kill, process::id(), SIGXFSZ);
233233
}
234234

235235
// Call SIGXCPU signal handler.
236-
assert_eq!(METRICS.signals.sigxcpu.count(), 0);
236+
assert_eq!(METRICS.signals.sigxcpu.fetch(), 0);
237237
unsafe {
238238
syscall(libc::SYS_kill, process::id(), SIGXCPU);
239239
}
@@ -245,13 +245,13 @@ mod tests {
245245
}
246246

247247
// Call SIGHUP signal handler.
248-
assert_eq!(METRICS.signals.sighup.count(), 0);
248+
assert_eq!(METRICS.signals.sighup.fetch(), 0);
249249
unsafe {
250250
syscall(libc::SYS_kill, process::id(), SIGHUP);
251251
}
252252

253253
// Call SIGILL signal handler.
254-
assert_eq!(METRICS.signals.sigill.count(), 0);
254+
assert_eq!(METRICS.signals.sigill.fetch(), 0);
255255
unsafe {
256256
syscall(libc::SYS_kill, process::id(), SIGILL);
257257
}
@@ -268,17 +268,17 @@ mod tests {
268268
// tests, so we use this as an heuristic to decide if we check the assertion.
269269
if cpu_count() > 1 {
270270
// The signal handler should let the program continue during unit tests.
271-
assert!(METRICS.seccomp.num_faults.count() >= 1);
271+
assert!(METRICS.seccomp.num_faults.fetch() >= 1);
272272
}
273-
assert!(METRICS.signals.sigbus.count() >= 1);
274-
assert!(METRICS.signals.sigsegv.count() >= 1);
275-
assert!(METRICS.signals.sigxfsz.count() >= 1);
276-
assert!(METRICS.signals.sigxcpu.count() >= 1);
273+
assert!(METRICS.signals.sigbus.fetch() >= 1);
274+
assert!(METRICS.signals.sigsegv.fetch() >= 1);
275+
assert!(METRICS.signals.sigxfsz.fetch() >= 1);
276+
assert!(METRICS.signals.sigxcpu.fetch() >= 1);
277277
assert!(METRICS.signals.sigpipe.count() >= 1);
278-
assert!(METRICS.signals.sighup.count() >= 1);
278+
assert!(METRICS.signals.sighup.fetch() >= 1);
279279
// Workaround to GitHub issue 2216.
280280
#[cfg(not(target_arch = "aarch64"))]
281-
assert!(METRICS.signals.sigill.count() >= 1);
281+
assert!(METRICS.signals.sigill.fetch() >= 1);
282282
}
283283

284284
fn make_test_seccomp_bpf_filter() -> Vec<sock_filter> {

tests/integration_tests/build/test_coverage.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@
3131
if utils.is_io_uring_supported():
3232
COVERAGE_DICT = {"Intel": 85.09, "AMD": 84.51, "ARM": 84.21}
3333
else:
34-
COVERAGE_DICT = {"Intel": 82.03, "AMD": 81.5, "ARM": 81.10}
34+
COVERAGE_DICT = {"Intel": 82.03, "AMD": 81.5, "ARM": 81.16}
3535

3636
PROC_MODEL = proc.proc_type()
3737

tests/integration_tests/security/test_custom_seccomp.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -205,7 +205,7 @@ def test_failing_filter(test_microvm_with_api):
205205
for line in lines:
206206
num_faults += json.loads(line)["seccomp"]["num_faults"]
207207

208-
assert num_faults == 1
208+
assert num_faults >= 1
209209

210210
# assert that the process was killed
211211
assert not psutil.pid_exists(test_microvm.jailer_clone_pid)

0 commit comments

Comments
 (0)