From 6bf0a5cb5034a7e684dcc3500e841785237ce2dd Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Sun, 7 Apr 2024 19:32:43 +0200 Subject: Adding upstream version 1:115.7.0. Signed-off-by: Daniel Baumann --- toolkit/xre/detect_win32k_conflicts/.gitignore | 2 + toolkit/xre/detect_win32k_conflicts/Cargo.toml | 10 + toolkit/xre/detect_win32k_conflicts/src/detect.rs | 165 ++++++++++ toolkit/xre/detect_win32k_conflicts/src/error.rs | 36 +++ toolkit/xre/detect_win32k_conflicts/src/ffi.rs | 57 ++++ toolkit/xre/detect_win32k_conflicts/src/lib.rs | 18 ++ .../xre/detect_win32k_conflicts/src/registry.rs | 360 +++++++++++++++++++++ toolkit/xre/detect_win32k_conflicts/tests/basic.rs | 14 + 8 files changed, 662 insertions(+) create mode 100644 toolkit/xre/detect_win32k_conflicts/.gitignore create mode 100644 toolkit/xre/detect_win32k_conflicts/Cargo.toml create mode 100644 toolkit/xre/detect_win32k_conflicts/src/detect.rs create mode 100644 toolkit/xre/detect_win32k_conflicts/src/error.rs create mode 100644 toolkit/xre/detect_win32k_conflicts/src/ffi.rs create mode 100644 toolkit/xre/detect_win32k_conflicts/src/lib.rs create mode 100644 toolkit/xre/detect_win32k_conflicts/src/registry.rs create mode 100644 toolkit/xre/detect_win32k_conflicts/tests/basic.rs (limited to 'toolkit/xre/detect_win32k_conflicts') diff --git a/toolkit/xre/detect_win32k_conflicts/.gitignore b/toolkit/xre/detect_win32k_conflicts/.gitignore new file mode 100644 index 0000000000..869df07dae --- /dev/null +++ b/toolkit/xre/detect_win32k_conflicts/.gitignore @@ -0,0 +1,2 @@ +/target +Cargo.lock \ No newline at end of file diff --git a/toolkit/xre/detect_win32k_conflicts/Cargo.toml b/toolkit/xre/detect_win32k_conflicts/Cargo.toml new file mode 100644 index 0000000000..a94402df9e --- /dev/null +++ b/toolkit/xre/detect_win32k_conflicts/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "detect_win32k_conflicts" +version = "0.1.0" +edition = "2021" +license = "MPL-2.0" + +[dependencies] +log = "0.4" +thiserror = "1" +winapi = { version = "0.3", features = ["winerror", "winreg"] } diff --git a/toolkit/xre/detect_win32k_conflicts/src/detect.rs b/toolkit/xre/detect_win32k_conflicts/src/detect.rs new file mode 100644 index 0000000000..e21c80e157 --- /dev/null +++ b/toolkit/xre/detect_win32k_conflicts/src/detect.rs @@ -0,0 +1,165 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +//! This module contains the actual functions that explore the registry for the status +//! of the conflicting mitigations. + +use super::error::DetectConflictError; +use super::registry::{RegKey, RegValue}; + +use std::ffi::OsStr; + +/// Subkey that the Windows Exploit Protection status is located in +const EXPLOIT_PROTECTION_SUBKEY: &str = + "SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\Image File Execution Options"; + +/// Name of the value within that subkey that contains the exploit protection status +const MITIGATION_VALUE_NAME: &str = "MitigationOptions"; + +/// The bit number within the value with the StackPivot status +const STACK_PIVOT_BIT: usize = 80; + +/// The bit number within the value with the CallerCheck status +const CALLER_CHECK_BIT: usize = 84; + +/// The bit number within the value with the SimExec status +const SIM_EXEC_BIT: usize = 88; + +/// Printable, FFI-safe structure containing the status of the conflicting mitigations +#[repr(C)] +#[derive(Clone, Copy, Debug, Default, PartialEq)] +pub struct ConflictingMitigationStatus { + /// The status of the CallerCheck feature. 0 = disabled, 1 = enabled + caller_check: bool, + /// The status of the SimExec feature. 0 = disabled, 1 = enabled + sim_exec: bool, + /// The status of the StackPivot feature. 0 = disabled, 1 = enabled + stack_pivot: bool, +} + +impl std::fmt::Display for ConflictingMitigationStatus { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> { + use std::fmt::Write; + write_status(f, "CallerCheck", self.caller_check)?; + f.write_char(' ')?; + write_status(f, "SimExec", self.sim_exec)?; + f.write_char(' ')?; + write_status(f, "StackPivot", self.stack_pivot) + } +} + +/// Search the registry for win32k-conflicting mitigations for the given process +/// +/// `process_name` is the name of the process that will be checked for conflicting mitigations +/// It uses the same case-insensitive algorithm that the Windows Registry uses itself to detect +/// whether a process needs mitigations or not +/// +/// (This will usually be `firefox.exe`) +/// +/// # Caveat +/// +/// Windows allows an arbitrary number of "filters", which are subkeys of the main key +/// that also have a `MitigationOptions` value. They are used to allow filtering by path. +/// +/// For simplicity, we "flatten" the path filters when we figure this out, and so the +/// returned value will be the logical "OR" of all mitigations enabled with any filter +/// +/// So if there are two entries for "C:\Firefox\firefox.exe" with mitigations `A` and `B` enabled +/// and another entry for "C:\Program Files\Mozilla Firefox\firefox.exe" with mitigation `C` +/// enabled, we will return that `A`, `B`, and `C` are all enabled +pub fn get_conflicting_mitigations( + process_name: impl AsRef, +) -> Result { + let process_name = process_name.as_ref(); + + let key = RegKey::root_local_machine() + .try_open_subkey(EXPLOIT_PROTECTION_SUBKEY)? + .ok_or(DetectConflictError::ExploitProtectionKeyMissing)?; + + let process_key = match key.try_open_subkey(process_name)? { + Some(key) => key, + None => { + log::info!( + "process name {:?} not found in exploit protection", + process_name + ); + return Ok(ConflictingMitigationStatus::default()); + } + }; + + // First get mitigation status for the root key + let mut status = get_conflicting_mitigations_for_key(&process_key)?; + + // Then get mitigation status for each subkey and logical-or them all together + for subkey_name in process_key.subkey_names() { + let subkey_name = subkey_name?; + + let subkey = process_key + .try_open_subkey(subkey_name)? + .expect("a subkey somehow doesn't exist after enumerating it"); + + let subkey_status = get_conflicting_mitigations_for_key(&subkey)?; + status.caller_check |= subkey_status.caller_check; + status.sim_exec |= subkey_status.sim_exec; + status.stack_pivot |= subkey_status.stack_pivot; + } + + log::info!( + "process name {:?} has mitigation status {:?}", + process_name, + status + ); + + Ok(status) +} + +/// Check a key to see if it has a "MitigationOptions" value with any conflicting mitigations +/// enabled +fn get_conflicting_mitigations_for_key( + key: &RegKey, +) -> Result { + let value = match key.try_get_value(MITIGATION_VALUE_NAME)? { + Some(value) => value, + None => return Ok(ConflictingMitigationStatus::default()), + }; + + let bits = match value { + RegValue::Binary(bits) => bits, + _ => return Ok(ConflictingMitigationStatus::default()), + }; + + Ok(ConflictingMitigationStatus { + caller_check: get_bit(&bits, CALLER_CHECK_BIT)?, + sim_exec: get_bit(&bits, SIM_EXEC_BIT)?, + stack_pivot: get_bit(&bits, STACK_PIVOT_BIT)?, + }) +} + +/// Retrieve a bit from a vector-of-bitflags, which is what will be returned by the registry +/// entry for exploit protection. The bits are numbered first by increasing value within a byte, +/// and then by increasing index within the slice. +/// +/// Ex: bit 0 = (index 0, mask 0x01), bit 7 = (index 0, mask 0x80), bit 8 = (index 1, mask 0x01) +fn get_bit(bytes: &[u8], bit_idx: usize) -> Result { + let byte = bytes + .get(bit_idx / 8) + .ok_or(DetectConflictError::RegValueTooShort)?; + + Ok(if byte & (1 << (bit_idx % 8)) != 0 { + true + } else { + false + }) +} + +/// Write the status of a feature to the given formatter. `bit == 0` means feature is disabled, +/// `bit == 1` means enabled +fn write_status( + f: &mut std::fmt::Formatter<'_>, + name: &str, + bit: bool, +) -> Result<(), std::fmt::Error> { + let status_str = if bit { "enabled" } else { "disabled" }; + write!(f, "{} is {}.", name, status_str) +} diff --git a/toolkit/xre/detect_win32k_conflicts/src/error.rs b/toolkit/xre/detect_win32k_conflicts/src/error.rs new file mode 100644 index 0000000000..cc73d04b2d --- /dev/null +++ b/toolkit/xre/detect_win32k_conflicts/src/error.rs @@ -0,0 +1,36 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +//! Contains the error type for this crate + +use thiserror::Error; + +/// The error type for this crate +#[derive(Debug, Error)] +pub enum DetectConflictError { + /// An exploit protection key was not found in the registry + #[error("exploit protection key missing")] + ExploitProtectionKeyMissing, + /// Failed to enumerate the next registry subkey + #[error("failed to enumerate next registry subkey. code: {0}")] + RegEnumKeyFailed(u32), + /// Failed to get a value from the registry + #[error("failed to get registry value. code: {0}")] + RegGetValueFailed(u32), + /// Failed to get the length of a value from the registry + #[error("failed to get registry value length. code: {0}")] + RegGetValueLenFailed(u32), + /// Failed to open a registry key + #[error("failed to open registry key. code: {0}")] + RegOpenKeyFailed(u32), + /// Exploit Protection registry value was too short + #[error("exploit protection registry value too short")] + RegValueTooShort, + /// Failed to query key for subkey length + #[error("failed to query key for max subkey length. code: {0}")] + RegQueryInfoKeyFailed(u32), + /// A registry value had an unsupported type + #[error("key has unsupported value type: {0}")] + UnsupportedValueType(u32), +} diff --git a/toolkit/xre/detect_win32k_conflicts/src/ffi.rs b/toolkit/xre/detect_win32k_conflicts/src/ffi.rs new file mode 100644 index 0000000000..826f452726 --- /dev/null +++ b/toolkit/xre/detect_win32k_conflicts/src/ffi.rs @@ -0,0 +1,57 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +//! C foreign-function interface for this crate + +use super::detect::{get_conflicting_mitigations, ConflictingMitigationStatus}; + +/// C FFI for `get_conflicting_mitigations` +/// +/// Checks the current process to see if any conflicting mitigations would be enabled +/// by Windows Exploit Protection +/// +/// `conflicting_mitigations` is an out pointer that will receive the status of the +/// Win32k conflicting mitigations +/// +/// On success, returns `1`. On failure, returns `0` and `conflicting_mitigations` is left +/// untouched +/// +/// # Safety +/// +/// `conflicting_mitigations` must not be a non-const, non-null ptr and **must be initialized** +/// +/// # Example +/// +/// ```C++ +/// void myFunc() { +/// ConflictingMitigationStatus status = {}; // GOOD - value-initializes the object +/// if(!detect_win32k_conflicting_mitigations(&status)) { +/// // error +/// } +/// } +/// ``` +#[no_mangle] +pub unsafe extern "C" fn detect_win32k_conflicting_mitigations( + conflicting_mitigations: &mut ConflictingMitigationStatus, +) -> bool { + let process_path = + std::env::current_exe().expect("unable to determine the path of our executable"); + let file_name = process_path + .file_name() + .expect("executable doesn't have a file name"); + + match get_conflicting_mitigations(file_name) { + Ok(status) => { + *conflicting_mitigations = status; + true + } + Err(e) => { + log::error!( + "Failed to get Win32k Lockdown conflicting mitigation status: {}", + e + ); + false + } + } +} diff --git a/toolkit/xre/detect_win32k_conflicts/src/lib.rs b/toolkit/xre/detect_win32k_conflicts/src/lib.rs new file mode 100644 index 0000000000..4333603b92 --- /dev/null +++ b/toolkit/xre/detect_win32k_conflicts/src/lib.rs @@ -0,0 +1,18 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +//! Crate to detect Windows Exploit Protection mitigations that may interfere with Win32k +//! Lockdown + +#![deny(missing_docs)] + +mod detect; +mod error; +mod ffi; +mod registry; + +pub use self::detect::get_conflicting_mitigations; +pub use self::detect::ConflictingMitigationStatus; +pub use self::error::DetectConflictError; +pub use self::ffi::detect_win32k_conflicting_mitigations; diff --git a/toolkit/xre/detect_win32k_conflicts/src/registry.rs b/toolkit/xre/detect_win32k_conflicts/src/registry.rs new file mode 100644 index 0000000000..f375a64284 --- /dev/null +++ b/toolkit/xre/detect_win32k_conflicts/src/registry.rs @@ -0,0 +1,360 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +//! This module contains helpers for exploring the Windows Registry + +use super::error::DetectConflictError; + +use std::ffi::{OsStr, OsString}; +use std::os::windows::ffi::{OsStrExt, OsStringExt}; + +use winapi::{ + shared::{ + minwindef::HKEY, + winerror::{ERROR_FILE_NOT_FOUND, ERROR_NO_MORE_ITEMS, ERROR_SUCCESS}, + }, + um::{ + winnt::{KEY_READ, REG_BINARY, REG_DWORD, REG_SZ}, + winreg::{ + RegCloseKey, RegEnumKeyExW, RegGetValueW, RegOpenKeyExW, RegQueryInfoKeyW, + HKEY_LOCAL_MACHINE, RRF_RT_REG_BINARY, RRF_RT_REG_DWORD, RRF_RT_REG_SZ, + }, + }, +}; + +/// An open key in the Windows Registry +/// +/// (Note that Windows ref-counts keys internally, so we don't need subkeys to "hang on" to +/// their parents) +#[derive(Debug)] +pub struct RegKey { + /// The Win32 handle of the open key + handle: HKEY, +} + +impl RegKey { + /// Open the global HKEY_LOCAL_MACHINE handle + pub fn root_local_machine() -> RegKey { + RegKey { + handle: HKEY_LOCAL_MACHINE, + } + } + /// Try to open a subkey relative to this key with read-only access rights + /// + /// Returns `None` if the subkey doesn't exist + pub fn try_open_subkey( + &self, + subkey: impl AsRef, + ) -> Result, DetectConflictError> { + let win32_subkey = to_win32_string(subkey.as_ref()); + + let mut subkey_handle = std::ptr::null_mut(); + let rv = unsafe { + RegOpenKeyExW( + self.handle, + win32_subkey.as_ptr(), + 0, // options + KEY_READ, + &mut subkey_handle, + ) + } + .try_into() + .unwrap(); + + match rv { + ERROR_SUCCESS => { + assert!(!subkey_handle.is_null()); + + Ok(Some(RegKey { + handle: subkey_handle, + })) + } + ERROR_FILE_NOT_FOUND => Ok(None), + _ => Err(DetectConflictError::RegOpenKeyFailed(rv)), + } + } + /// Try to get a value with the given name + /// + /// Returns `None` if there is no value with that name + /// + /// Note that you can pass the empty string to get the default value + pub fn try_get_value( + &self, + value_name: impl AsRef, + ) -> Result, DetectConflictError> { + let win32_value_name = to_win32_string(value_name.as_ref()); + + // First we query the value's length and type so we know how to create the data buffer + let mut value_type = 0; + let mut value_len = 0; + + let rv = unsafe { + RegGetValueW( + self.handle, + std::ptr::null(), // subkey + win32_value_name.as_ptr(), + RRF_RT_REG_BINARY | RRF_RT_REG_DWORD | RRF_RT_REG_SZ, + &mut value_type, + std::ptr::null_mut(), // no data ptr, just query length & type + &mut value_len, + ) + } + .try_into() + .unwrap(); + + if rv == ERROR_FILE_NOT_FOUND { + return Ok(None); + } + + if rv != ERROR_SUCCESS || value_len == 0 { + return Err(DetectConflictError::RegGetValueLenFailed(rv)); + } + + if value_type == REG_SZ { + // buffer length is in wide characters + let buffer_len = value_len / 2; + + assert_eq!(buffer_len * 2, value_len); + + let mut buffer = vec![0u16; buffer_len.try_into().unwrap()]; + + let rv = unsafe { + RegGetValueW( + self.handle, + std::ptr::null(), // subkey + win32_value_name.as_ptr(), + RRF_RT_REG_SZ, + std::ptr::null_mut(), // no need to query type again + buffer.as_mut_ptr().cast(), + &mut value_len, + ) + } + .try_into() + .unwrap(); + + if rv != ERROR_SUCCESS { + return Err(DetectConflictError::RegGetValueFailed(rv)); + } + + Ok(Some(RegValue::String(from_win32_string(&buffer)))) + } else if value_type == REG_BINARY { + let mut buffer = vec![0u8; value_len.try_into().unwrap()]; + + let rv = unsafe { + RegGetValueW( + self.handle, + std::ptr::null(), // subkey + win32_value_name.as_ptr(), + RRF_RT_REG_BINARY, + std::ptr::null_mut(), // no need to query type again + buffer.as_mut_ptr().cast(), + &mut value_len, + ) + } + .try_into() + .unwrap(); + + if rv != ERROR_SUCCESS { + return Err(DetectConflictError::RegGetValueFailed(rv)); + } + + Ok(Some(RegValue::Binary(buffer))) + } else if value_type == REG_DWORD { + assert_eq!(value_len, 4); + + let mut buffer = 0u32; + + let rv = unsafe { + RegGetValueW( + self.handle, + std::ptr::null(), // subkey + win32_value_name.as_ptr(), + RRF_RT_REG_DWORD, + std::ptr::null_mut(), // no need to query type again + (&mut buffer as *mut u32).cast(), + &mut value_len, + ) + } + .try_into() + .unwrap(); + + if rv != ERROR_SUCCESS { + return Err(DetectConflictError::RegGetValueFailed(rv)); + } + + Ok(Some(RegValue::Dword(buffer))) + } else { + Err(DetectConflictError::UnsupportedValueType(value_type)) + } + } + /// Get an iterator that returns the names of all the subkeys of this key + pub fn subkey_names(&self) -> SubkeyNames<'_> { + SubkeyNames::new(self) + } +} + +impl Drop for RegKey { + fn drop(&mut self) { + assert!(!self.handle.is_null()); + + if self.handle == HKEY_LOCAL_MACHINE { + return; + } + + let rv: u32 = unsafe { RegCloseKey(self.handle) }.try_into().unwrap(); + + if rv != ERROR_SUCCESS { + log::warn!("failed to close opened registry key"); + } + } +} + +/// An iterator through the subkeys of a key +/// +/// Returns items of type `Result`. If an error occurs, calling `next()` again +/// will likely just continue to result in errors, so it's best to just abandon ship at that +/// point +#[derive(Debug)] +pub struct SubkeyNames<'a> { + /// The key that is being iterated + key: &'a RegKey, + /// The current index of the subkey -- Incremented each successful call to `next()` + index: u32, + /// A buffer that is large enough to hold the longest subkey name + buffer: Option>, +} + +impl<'a> SubkeyNames<'a> { + /// Create a new subkey name iterator + fn new(key: &'a RegKey) -> SubkeyNames<'a> { + SubkeyNames { + key, + index: 0, + buffer: None, + } + } + /// Initialize `self.buffer` with a vector that's long enough to hold the longest subkey name + /// (if it isn't already) + fn create_buffer_if_needed(&mut self) -> Result<(), DetectConflictError> { + if self.buffer.is_some() { + return Ok(()); + } + + let mut max_key_length = 0; + + let rv = unsafe { + RegQueryInfoKeyW( + self.key.handle, + std::ptr::null_mut(), // class ptr + std::ptr::null_mut(), // class len ptr + std::ptr::null_mut(), // reserved + std::ptr::null_mut(), // num of subkeys ptr + &mut max_key_length, + std::ptr::null_mut(), // max class length ptr + std::ptr::null_mut(), // number of values ptr + std::ptr::null_mut(), // longest value name ptr + std::ptr::null_mut(), // longest value data ptr + std::ptr::null_mut(), // DACL ptr + std::ptr::null_mut(), // last write time ptr + ) + } + .try_into() + .unwrap(); + + if rv != ERROR_SUCCESS { + return Err(DetectConflictError::RegQueryInfoKeyFailed(rv)); + } + + // +1 for the null terminator + let max_key_length = usize::try_from(max_key_length).unwrap() + 1; + + self.buffer = Some(vec![0u16; max_key_length]); + + Ok(()) + } +} + +impl<'a> Iterator for SubkeyNames<'a> { + type Item = Result; + + fn next(&mut self) -> Option { + if let Err(e) = self.create_buffer_if_needed() { + return Some(Err(e)); + } + + let buffer = self.buffer.as_mut().unwrap(); + let mut name_len = u32::try_from(buffer.len()).unwrap(); + + let rv = unsafe { + RegEnumKeyExW( + self.key.handle, + self.index, + buffer.as_mut_ptr(), + &mut name_len, + std::ptr::null_mut(), // reserved + std::ptr::null_mut(), // class ptr + std::ptr::null_mut(), // class len ptr + std::ptr::null_mut(), // last write time ptr + ) + } + .try_into() + .unwrap(); + + if rv == ERROR_NO_MORE_ITEMS { + return None; + } + + if rv != ERROR_SUCCESS { + return Some(Err(DetectConflictError::RegEnumKeyFailed(rv))); + } + + self.index += 1; + + // +1 for the null terminator + let name_len = usize::try_from(name_len).unwrap() + 1; + + Some(Ok(from_win32_string(&buffer[0..name_len]))) + } +} + +/// A single value in the registry +#[derive(Clone, Debug, PartialEq)] +pub enum RegValue { + /// A REG_BINARY value + Binary(Vec), + /// A REG_DWORD value + Dword(u32), + /// A REG_SZ value + String(OsString), +} + +/// Convert an OsStr to a null-terminated wide character string +/// +/// The input string must not contain any interior null values, and the resulting wide +/// character array will have a null terminator appended +fn to_win32_string(s: &OsStr) -> Vec { + // +1 for the null terminator + let mut result = Vec::with_capacity(s.len() + 1); + + for wc in s.encode_wide() { + assert_ne!(wc, 0); + result.push(wc); + } + + result.push(0); + result +} + +/// Convert a null-terminated wide character string into an OsString +/// +/// The passed-in slice must have exactly one wide null character as the last element +fn from_win32_string(s: &[u16]) -> OsString { + for (idx, wc) in s.iter().enumerate() { + if *wc == 0 { + assert_eq!(idx, s.len() - 1); + return OsString::from_wide(&s[0..idx]); + } + } + panic!("missing null terminator at end of win32 string"); +} diff --git a/toolkit/xre/detect_win32k_conflicts/tests/basic.rs b/toolkit/xre/detect_win32k_conflicts/tests/basic.rs new file mode 100644 index 0000000000..d16a0bd976 --- /dev/null +++ b/toolkit/xre/detect_win32k_conflicts/tests/basic.rs @@ -0,0 +1,14 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +use detect_win32k_conflicts::{detect_win32k_conflicting_mitigations, ConflictingMitigationStatus}; + +#[test] +fn basic() { + let mut status = ConflictingMitigationStatus::default(); + + let result = unsafe { detect_win32k_conflicting_mitigations(&mut status) }; + + assert!(result); +} -- cgit v1.2.3