Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion src/hyperlight_host/src/sandbox/host_funcs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,9 @@ fn maybe_with_seccomp<T: Send>(
.name(format!("Host Function Worker Thread for: {name:?}"))
.spawn(move |_| {
let seccomp_filter = get_seccomp_filter_for_host_function_worker_thread(syscalls)?;
seccompiler::apply_filter(&seccomp_filter)?;
seccomp_filter
.iter()
.try_for_each(|filter| seccompiler::apply_filter(filter))?;

// We have a `catch_unwind` here because, if a disallowed syscall is issued,
// we handle it by panicking. This is to avoid returning execution to the
Expand Down
63 changes: 63 additions & 0 deletions src/hyperlight_host/src/sandbox/initialized_multi_use.rs
Original file line number Diff line number Diff line change
Expand Up @@ -458,6 +458,69 @@ mod tests {
Ok(())
}

// We have a secomp specifically for `openat`, but we don't want to crash on `openat`, but rather make sure `openat` returns `EACCES`
#[test]
#[cfg(target_os = "linux")]
fn violate_seccomp_filters_openat() -> Result<()> {
// Hostcall to call `openat`
fn make_openat_syscall() -> Result<i64> {
use std::ffi::CString;

let path = CString::new("/proc/sys/vm/overcommit_memory").unwrap();

let fd_or_err = unsafe {
libc::syscall(
libc::SYS_openat,
libc::AT_FDCWD,
path.as_ptr(),
libc::O_RDONLY,
)
};

if fd_or_err == -1 {
Ok(std::io::Error::last_os_error()
.raw_os_error()
.unwrap()
.into())
} else {
Ok(fd_or_err)
}
}

// First make sure a regular call to `openat` on /proc/sys/vm/overcommit_memory succeeds
let ret = make_openat_syscall()?;
assert!(
ret >= 0,
"Expected openat syscall to succeed, got: {:?}",
ret
);

let mut ubox = UninitializedSandbox::new(
GuestBinary::FilePath(simple_guest_as_string().expect("Guest Binary Missing")),
None,
)
.unwrap();
ubox.register("Openat_Hostfunc", make_openat_syscall)?;

let mut sbox = ubox.evolve(Noop::default()).unwrap();
let host_func_result = sbox
.call_guest_function_by_name::<i64>(
"CallGivenParamlessHostFuncThatReturnsI64",
"Openat_Hostfunc".to_string(),
)
.expect("Expected to call host function that returns i64");

if cfg!(feature = "seccomp") {
// If seccomp is enabled, we expect the syscall to return EACCES, as setup by our seccomp filter
assert_eq!(host_func_result, libc::EACCES as i64);
} else {
// If seccomp is not enabled, we expect the syscall to succeed
assert!(host_func_result >= 0);
}

Ok(())
}

#[test]
fn test_trigger_exception_on_guest() {
let usbox = UninitializedSandbox::new(
Expand Down
53 changes: 44 additions & 9 deletions src/hyperlight_host/src/seccomp/guest.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ limitations under the License.
use seccompiler::SeccompCmpOp::Eq;
use seccompiler::{
BpfProgram, SeccompAction, SeccompCmpArgLen as ArgLen, SeccompCondition as Cond, SeccompFilter,
SeccompRule,
SeccompRule, TargetArch,
};

use crate::sandbox::ExtraAllowedSyscall;
Expand Down Expand Up @@ -59,10 +59,14 @@ fn syscalls_allowlist() -> Result<Vec<(i64, Vec<SeccompRule>)>> {
(libc::SYS_sched_yield, vec![]),
// `mprotect` is needed by malloc during memory allocation
(libc::SYS_mprotect, vec![]),
// `openat` is marked allowed here because it may be called by `libc::free()`
// since it will try to open /proc/sys/vm/overcommit_memory (https://sourceware.org/git/?p=glibc.git;a=blob;f=sysdeps/unix/sysv/linux/malloc-sysdep.h;h=778d8971d53e284397c3a5dcdd923e93be5e4731;hb=HEAD)
// We have another more restrictive filter for it below so it will return EACCES instead of trap, in which case libc will use the default value
(libc::SYS_openat, vec![]),
])
}

/// Creates a `BpfProgram` for a `SeccompFilter` over specific syscalls/`SeccompRule`s
/// Creates two `BpfProgram`s for a `SeccompFilter` over specific syscalls/`SeccompRule`s
/// intended to be applied on host function threads.
///
/// Note: This does not provide coverage over the Hyperlight host, which is why we don't need
Expand All @@ -71,7 +75,7 @@ fn syscalls_allowlist() -> Result<Vec<(i64, Vec<SeccompRule>)>> {
/// or `KVM_CREATE_VCPU`).
pub(crate) fn get_seccomp_filter_for_host_function_worker_thread(
extra_allowed_syscalls: Option<&[ExtraAllowedSyscall]>,
) -> Result<BpfProgram> {
) -> Result<Vec<BpfProgram>> {
let mut allowed_syscalls = syscalls_allowlist()?;

if let Some(extra_allowed_syscalls) = extra_allowed_syscalls {
Expand All @@ -87,11 +91,42 @@ pub(crate) fn get_seccomp_filter_for_host_function_worker_thread(
allowed_syscalls.dedup();
}

Ok(SeccompFilter::new(
let arch: TargetArch = std::env::consts::ARCH.try_into()?;

// First: Filter that forces `openat` to return EACCES
let errno_on_openat = SeccompFilter::new(
[(libc::SYS_openat, vec![])].into_iter().collect(),
SeccompAction::Allow,
SeccompAction::Errno(libc::EACCES.try_into()?),
arch,
)?
.try_into()?;

// Second: Allowlist filter that traps on unknown syscalls
let allowlist = SeccompFilter::new(
allowed_syscalls.into_iter().collect(),
SeccompAction::Trap, // non-match syscall will kill the offending thread
SeccompAction::Allow, // match syscall will be allowed
std::env::consts::ARCH.try_into()?,
)
.and_then(|filter| filter.try_into())?)
SeccompAction::Trap,
SeccompAction::Allow,
arch,
)?
.try_into()?;

// Note: the order of the 2 filters are important. If we applied the strict filter first,
// we wouldn't be allowed to setup the second filter (would be trapped since the syscalls to setup seccomp are not allowed).
// However, from an seccomp filter perspective, the order of the filters is not important:
//
// If multiple filters exist, they are all executed, in reverse order
// of their addition to the filter tree—that is, the most recently
// installed filter is executed first. (Note that all filters will
// be called even if one of the earlier filters returns
// SECCOMP_RET_KILL. This is done to simplify the kernel code and to
// provide a tiny speed-up in the execution of sets of filters by
// avoiding a check for this uncommon case.) The return value for
// the evaluation of a given system call is the first-seen action
// value of highest precedence (along with its accompanying data)
// returned by execution of all of the filters.
//
// (https://man7.org/linux/man-pages/man2/seccomp.2.html).
//
Ok(vec![errno_on_openat, allowlist])
}
23 changes: 23 additions & 0 deletions src/tests/rust_guests/simpleguest/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -694,6 +694,21 @@ fn violate_seccomp_filters(function_call: &FunctionCall) -> Result<Vec<u8>> {
}
}

fn call_given_paramless_hostfunc_that_returns_i64(function_call: &FunctionCall) -> Result<Vec<u8>> {
if let ParameterValue::String(hostfuncname) =
function_call.parameters.clone().unwrap()[0].clone()
{
let res = call_host_function::<i64>(&hostfuncname, None, ReturnType::Long)?;

Ok(get_flatbuffer_result(res))
} else {
Err(HyperlightGuestError::new(
ErrorCode::GuestFunctionParameterTypeMismatch,
"Invalid parameters passed to test_rust_malloc".to_string(),
))
}
}

fn add(function_call: &FunctionCall) -> Result<Vec<u8>> {
if let (ParameterValue::Int(a), ParameterValue::Int(b)) = (
function_call.parameters.clone().unwrap()[0].clone(),
Expand Down Expand Up @@ -1131,6 +1146,14 @@ pub extern "C" fn hyperlight_main() {
large_parameters as usize,
);
register_function(large_parameters_def);

let call_given_hostfunc_def = GuestFunctionDefinition::new(
"CallGivenParamlessHostFuncThatReturnsI64".to_string(),
Vec::from(&[ParameterType::String]),
ReturnType::Long,
call_given_paramless_hostfunc_that_returns_i64 as usize,
);
register_function(call_given_hostfunc_def);
}

#[no_mangle]
Expand Down
Loading