dragonball: add signal handler

Used to register dragonball's signal handler.

Signed-off-by: Liu Jiang <gerry@linux.alibaba.com>
Signed-off-by: jingshan <jingshan@linux.alibaba.com>
Signed-off-by: Chao Wu <chaowu@linux.alibaba.com>
Signed-off-by: wllenyj <wllenyj@linux.alibaba.com>
This commit is contained in:
wllenyj 2022-05-14 23:17:19 +08:00 committed by Chao Wu
parent b6cb2c4ae3
commit e89e6507a4
3 changed files with 242 additions and 0 deletions

View File

@ -27,6 +27,7 @@ libc = "0.2.39"
linux-loader = "0.4.0"
log = "0.4.14"
nix = "0.23.1"
seccompiler = "0.2.0"
serde = "1.0.27"
serde_derive = "1.0.27"
serde_json = "1.0.9"

View File

@ -1,4 +1,5 @@
// Copyright (C) 2018-2022 Alibaba Cloud. All rights reserved.
// Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
//! Dragonball is a light-weight virtual machine manager(VMM) based on Linux Kernel-based Virtual
@ -17,6 +18,8 @@ pub mod config_manager;
pub mod device_manager;
/// Errors related to Virtual machine manager.
pub mod error;
/// Signal handler for virtual machines.
pub mod signal_handler;
/// Metrics system.
pub mod metric;
/// Resource manager for virtual machines.
@ -26,3 +29,22 @@ pub mod vm;
mod io_manager;
pub use self::io_manager::IoManagerCached;
/// Success exit code.
pub const EXIT_CODE_OK: u8 = 0;
/// Generic error exit code.
pub const EXIT_CODE_GENERIC_ERROR: u8 = 1;
/// Generic exit code for an error considered not possible to occur if the program logic is sound.
pub const EXIT_CODE_UNEXPECTED_ERROR: u8 = 2;
/// Dragonball was shut down after intercepting a restricted system call.
pub const EXIT_CODE_BAD_SYSCALL: u8 = 148;
/// Dragonball was shut down after intercepting `SIGBUS`.
pub const EXIT_CODE_SIGBUS: u8 = 149;
/// Dragonball was shut down after intercepting `SIGSEGV`.
pub const EXIT_CODE_SIGSEGV: u8 = 150;
/// Invalid json passed to the Dragonball process for configuring microvm.
pub const EXIT_CODE_INVALID_JSON: u8 = 151;
/// Bad configuration for microvm's resources, when using a single json.
pub const EXIT_CODE_BAD_CONFIGURATION: u8 = 152;
/// Command line arguments parsing error.
pub const EXIT_CODE_ARG_PARSING: u8 = 153;

View File

@ -0,0 +1,219 @@
// Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
use libc::{_exit, c_int, c_void, siginfo_t, SIGBUS, SIGSEGV, SIGSYS};
use log::error;
use vmm_sys_util::signal::register_signal_handler;
use crate::metric::{IncMetric, METRICS};
// The offset of `si_syscall` (offending syscall identifier) within the siginfo structure
// expressed as an `(u)int*`.
// Offset `6` for an `i32` field means that the needed information is located at `6 * sizeof(i32)`.
// See /usr/include/linux/signal.h for the C struct definition.
// See https://github.com/rust-lang/libc/issues/716 for why the offset is different in Rust.
const SI_OFF_SYSCALL: isize = 6;
const SYS_SECCOMP_CODE: i32 = 1;
extern "C" {
fn __libc_current_sigrtmin() -> c_int;
fn __libc_current_sigrtmax() -> c_int;
}
/// Gets current sigrtmin
pub fn sigrtmin() -> c_int {
unsafe { __libc_current_sigrtmin() }
}
/// Gets current sigrtmax
pub fn sigrtmax() -> c_int {
unsafe { __libc_current_sigrtmax() }
}
/// Signal handler for `SIGSYS`.
///
/// Increments the `seccomp.num_faults` metric, logs an error message and terminates the process
/// with a specific exit code.
extern "C" fn sigsys_handler(num: c_int, info: *mut siginfo_t, _unused: *mut c_void) {
// Safe because we're just reading some fields from a supposedly valid argument.
let si_signo = unsafe { (*info).si_signo };
let si_code = unsafe { (*info).si_code };
// Sanity check. The condition should never be true.
if num != si_signo || num != SIGSYS || si_code != SYS_SECCOMP_CODE as i32 {
// Safe because we're terminating the process anyway.
unsafe { _exit(i32::from(super::EXIT_CODE_UNEXPECTED_ERROR)) };
}
// Other signals which might do async unsafe things incompatible with the rest of this
// function are blocked due to the sa_mask used when registering the signal handler.
let syscall = unsafe { *(info as *const i32).offset(SI_OFF_SYSCALL) as usize };
// SIGSYS is triggered when bad syscalls are detected. num_faults is only added when SIGSYS is detected
// so it actually only collects the count for bad syscalls.
METRICS.seccomp.num_faults.inc();
error!(
"Shutting down VM after intercepting a bad syscall ({}).",
syscall
);
// Safe because we're terminating the process anyway. We don't actually do anything when
// running unit tests.
#[cfg(not(test))]
unsafe {
_exit(i32::from(super::EXIT_CODE_BAD_SYSCALL))
};
}
/// Signal handler for `SIGBUS` and `SIGSEGV`.
///
/// Logs an error message and terminates the process with a specific exit code.
extern "C" fn sigbus_sigsegv_handler(num: c_int, info: *mut siginfo_t, _unused: *mut c_void) {
// Safe because we're just reading some fields from a supposedly valid argument.
let si_signo = unsafe { (*info).si_signo };
let si_code = unsafe { (*info).si_code };
// Sanity check. The condition should never be true.
if num != si_signo || (num != SIGBUS && num != SIGSEGV) {
// Safe because we're terminating the process anyway.
unsafe { _exit(i32::from(super::EXIT_CODE_UNEXPECTED_ERROR)) };
}
// Other signals which might do async unsafe things incompatible with the rest of this
// function are blocked due to the sa_mask used when registering the signal handler.
match si_signo {
SIGBUS => METRICS.signals.sigbus.inc(),
SIGSEGV => METRICS.signals.sigsegv.inc(),
_ => (),
}
error!(
"Shutting down VM after intercepting signal {}, code {}.",
si_signo, si_code
);
// Safe because we're terminating the process anyway. We don't actually do anything when
// running unit tests.
#[cfg(not(test))]
unsafe {
_exit(i32::from(match si_signo {
SIGBUS => super::EXIT_CODE_SIGBUS,
SIGSEGV => super::EXIT_CODE_SIGSEGV,
_ => super::EXIT_CODE_UNEXPECTED_ERROR,
}))
};
}
/// Registers all the required signal handlers.
///
/// Custom handlers are installed for: `SIGBUS`, `SIGSEGV`, `SIGSYS`.
pub fn register_signal_handlers() -> vmm_sys_util::errno::Result<()> {
// Call to unsafe register_signal_handler which is considered unsafe because it will
// register a signal handler which will be called in the current thread and will interrupt
// whatever work is done on the current thread, so we have to keep in mind that the registered
// signal handler must only do async-signal-safe operations.
register_signal_handler(SIGSYS, sigsys_handler)?;
register_signal_handler(SIGBUS, sigbus_sigsegv_handler)?;
register_signal_handler(SIGSEGV, sigbus_sigsegv_handler)?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use libc::{cpu_set_t, syscall};
use std::convert::TryInto;
use std::{mem, process, thread};
use seccompiler::{apply_filter, BpfProgram, SeccompAction, SeccompFilter};
// This function is used when running unit tests, so all the unsafes are safe.
fn cpu_count() -> usize {
let mut cpuset: cpu_set_t = unsafe { mem::zeroed() };
unsafe {
libc::CPU_ZERO(&mut cpuset);
}
let ret = unsafe {
libc::sched_getaffinity(
0,
mem::size_of::<cpu_set_t>(),
&mut cpuset as *mut cpu_set_t,
)
};
assert_eq!(ret, 0);
let mut num = 0;
for i in 0..libc::CPU_SETSIZE as usize {
if unsafe { libc::CPU_ISSET(i, &cpuset) } {
num += 1;
}
}
num
}
#[test]
fn test_signal_handler() {
let child = thread::spawn(move || {
assert!(register_signal_handlers().is_ok());
let filter = SeccompFilter::new(
vec![
(libc::SYS_brk, vec![]),
(libc::SYS_exit, vec![]),
(libc::SYS_futex, vec![]),
(libc::SYS_getpid, vec![]),
(libc::SYS_munmap, vec![]),
(libc::SYS_kill, vec![]),
(libc::SYS_rt_sigprocmask, vec![]),
(libc::SYS_rt_sigreturn, vec![]),
(libc::SYS_sched_getaffinity, vec![]),
(libc::SYS_set_tid_address, vec![]),
(libc::SYS_sigaltstack, vec![]),
(libc::SYS_write, vec![]),
]
.into_iter()
.collect(),
SeccompAction::Trap,
SeccompAction::Allow,
std::env::consts::ARCH.try_into().unwrap(),
)
.unwrap();
assert!(apply_filter(&TryInto::<BpfProgram>::try_into(filter).unwrap()).is_ok());
assert_eq!(METRICS.seccomp.num_faults.count(), 0);
// Call the blacklisted `SYS_mkdirat`.
unsafe { syscall(libc::SYS_mkdirat, "/foo/bar\0") };
// Call SIGBUS signal handler.
assert_eq!(METRICS.signals.sigbus.count(), 0);
unsafe {
syscall(libc::SYS_kill, process::id(), SIGBUS);
}
// Call SIGSEGV signal handler.
assert_eq!(METRICS.signals.sigsegv.count(), 0);
unsafe {
syscall(libc::SYS_kill, process::id(), SIGSEGV);
}
});
assert!(child.join().is_ok());
// Sanity check.
assert!(cpu_count() > 0);
// Kcov somehow messes with our handler getting the SIGSYS signal when a bad syscall
// is caught, so the following assertion no longer holds. Ideally, we'd have a surefire
// way of either preventing this behaviour, or detecting for certain whether this test is
// run by kcov or not. The best we could do so far is to look at the perceived number of
// available CPUs. Kcov seems to make a single CPU available to the process running the
// tests, so we use this as an heuristic to decide if we check the assertion.
if cpu_count() > 1 {
// The signal handler should let the program continue during unit tests.
assert!(METRICS.seccomp.num_faults.count() >= 1);
}
assert!(METRICS.signals.sigbus.count() >= 1);
assert!(METRICS.signals.sigsegv.count() >= 1);
}
}