TimeoutInprocessForkExecutor (#797)
* TimeoutInprocessForkExecutor * no_std * linux only * OK * crash -> timeout
This commit is contained in:
parent
3489e9aeaa
commit
caa560b7a0
@ -11,8 +11,10 @@ use core::{
|
|||||||
ffi::c_void,
|
ffi::c_void,
|
||||||
fmt::{self, Debug, Formatter},
|
fmt::{self, Debug, Formatter},
|
||||||
marker::PhantomData,
|
marker::PhantomData,
|
||||||
ptr,
|
ptr::{self, null_mut},
|
||||||
};
|
};
|
||||||
|
#[cfg(all(target_os = "linux", feature = "std"))]
|
||||||
|
use core::{ptr::addr_of_mut, time::Duration};
|
||||||
#[cfg(any(unix, all(windows, feature = "std")))]
|
#[cfg(any(unix, all(windows, feature = "std")))]
|
||||||
use core::{
|
use core::{
|
||||||
ptr::write_volatile,
|
ptr::write_volatile,
|
||||||
@ -446,11 +448,11 @@ impl InProcessExecutorHandlerData {
|
|||||||
/// Exception handling needs some nasty unsafe.
|
/// Exception handling needs some nasty unsafe.
|
||||||
pub(crate) static mut GLOBAL_STATE: InProcessExecutorHandlerData = InProcessExecutorHandlerData {
|
pub(crate) static mut GLOBAL_STATE: InProcessExecutorHandlerData = InProcessExecutorHandlerData {
|
||||||
/// The state ptr for signal handling
|
/// The state ptr for signal handling
|
||||||
state_ptr: ptr::null_mut(),
|
state_ptr: null_mut(),
|
||||||
/// The event manager ptr for signal handling
|
/// The event manager ptr for signal handling
|
||||||
event_mgr_ptr: ptr::null_mut(),
|
event_mgr_ptr: null_mut(),
|
||||||
/// The fuzzer ptr for signal handling
|
/// The fuzzer ptr for signal handling
|
||||||
fuzzer_ptr: ptr::null_mut(),
|
fuzzer_ptr: null_mut(),
|
||||||
/// The executor ptr for signal handling
|
/// The executor ptr for signal handling
|
||||||
executor_ptr: ptr::null(),
|
executor_ptr: ptr::null(),
|
||||||
/// The current input for signal handling
|
/// The current input for signal handling
|
||||||
@ -460,13 +462,13 @@ pub(crate) static mut GLOBAL_STATE: InProcessExecutorHandlerData = InProcessExec
|
|||||||
/// The timeout handler fn
|
/// The timeout handler fn
|
||||||
timeout_handler: ptr::null(),
|
timeout_handler: ptr::null(),
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
tp_timer: ptr::null_mut(),
|
tp_timer: null_mut(),
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
in_target: 0,
|
in_target: 0,
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
critical: ptr::null_mut(),
|
critical: null_mut(),
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
timeout_input_ptr: ptr::null_mut(),
|
timeout_input_ptr: null_mut(),
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Get the inprocess [`crate::state::State`]
|
/// Get the inprocess [`crate::state::State`]
|
||||||
@ -1254,6 +1256,8 @@ pub(crate) type ForkHandlerFuncPtr =
|
|||||||
pub struct InChildProcessHandlers {
|
pub struct InChildProcessHandlers {
|
||||||
/// On crash C function pointer
|
/// On crash C function pointer
|
||||||
pub crash_handler: *const c_void,
|
pub crash_handler: *const c_void,
|
||||||
|
/// On timeout C function pointer
|
||||||
|
pub timeout_handler: *const c_void,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(all(feature = "std", unix))]
|
#[cfg(all(feature = "std", unix))]
|
||||||
@ -1272,6 +1276,7 @@ impl InChildProcessHandlers {
|
|||||||
);
|
);
|
||||||
write_volatile(&mut data.state_ptr, state as *mut _ as *mut c_void);
|
write_volatile(&mut data.state_ptr, state as *mut _ as *mut c_void);
|
||||||
data.crash_handler = self.crash_handler;
|
data.crash_handler = self.crash_handler;
|
||||||
|
data.timeout_handler = self.timeout_handler;
|
||||||
compiler_fence(Ordering::SeqCst);
|
compiler_fence(Ordering::SeqCst);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1285,12 +1290,34 @@ impl InChildProcessHandlers {
|
|||||||
{
|
{
|
||||||
unsafe {
|
unsafe {
|
||||||
let data = &mut FORK_EXECUTOR_GLOBAL_DATA;
|
let data = &mut FORK_EXECUTOR_GLOBAL_DATA;
|
||||||
child_signal_handlers::setup_child_panic_hook::<E, I, OT, S>();
|
// child_signal_handlers::setup_child_panic_hook::<E, I, OT, S>();
|
||||||
setup_signal_handler(data)?;
|
setup_signal_handler(data)?;
|
||||||
compiler_fence(Ordering::SeqCst);
|
compiler_fence(Ordering::SeqCst);
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
crash_handler: child_signal_handlers::child_crash_handler::<E, I, OT, S>
|
crash_handler: child_signal_handlers::child_crash_handler::<E, I, OT, S>
|
||||||
as *const c_void,
|
as *const c_void,
|
||||||
|
timeout_handler: ptr::null(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create new [`InChildProcessHandlers`].
|
||||||
|
pub fn with_timeout<E, I, OT, S>() -> Result<Self, Error>
|
||||||
|
where
|
||||||
|
I: Input,
|
||||||
|
E: HasObservers<I, OT, S>,
|
||||||
|
OT: ObserversTuple<I, S>,
|
||||||
|
{
|
||||||
|
unsafe {
|
||||||
|
let data = &mut FORK_EXECUTOR_GLOBAL_DATA;
|
||||||
|
// child_signal_handlers::setup_child_panic_hook::<E, I, OT, S>();
|
||||||
|
setup_signal_handler(data)?;
|
||||||
|
compiler_fence(Ordering::SeqCst);
|
||||||
|
Ok(Self {
|
||||||
|
crash_handler: child_signal_handlers::child_crash_handler::<E, I, OT, S>
|
||||||
|
as *const c_void,
|
||||||
|
timeout_handler: child_signal_handlers::child_timeout_handler::<E, I, OT, S>
|
||||||
|
as *const c_void,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1300,6 +1327,7 @@ impl InChildProcessHandlers {
|
|||||||
pub fn nop() -> Self {
|
pub fn nop() -> Self {
|
||||||
Self {
|
Self {
|
||||||
crash_handler: ptr::null(),
|
crash_handler: ptr::null(),
|
||||||
|
timeout_handler: ptr::null(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1316,6 +1344,8 @@ pub(crate) struct InProcessForkExecutorGlobalData {
|
|||||||
pub current_input_ptr: *const c_void,
|
pub current_input_ptr: *const c_void,
|
||||||
/// Stores a pointer to the crash_handler function
|
/// Stores a pointer to the crash_handler function
|
||||||
pub crash_handler: *const c_void,
|
pub crash_handler: *const c_void,
|
||||||
|
/// Stores a pointer to the timeout_handler function
|
||||||
|
pub timeout_handler: *const c_void,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(all(feature = "std", unix))]
|
#[cfg(all(feature = "std", unix))]
|
||||||
@ -1353,16 +1383,23 @@ impl InProcessForkExecutorGlobalData {
|
|||||||
pub(crate) static mut FORK_EXECUTOR_GLOBAL_DATA: InProcessForkExecutorGlobalData =
|
pub(crate) static mut FORK_EXECUTOR_GLOBAL_DATA: InProcessForkExecutorGlobalData =
|
||||||
InProcessForkExecutorGlobalData {
|
InProcessForkExecutorGlobalData {
|
||||||
executor_ptr: ptr::null(),
|
executor_ptr: ptr::null(),
|
||||||
crash_handler: ptr::null(),
|
|
||||||
state_ptr: ptr::null(),
|
state_ptr: ptr::null(),
|
||||||
current_input_ptr: ptr::null(),
|
current_input_ptr: ptr::null(),
|
||||||
|
crash_handler: ptr::null(),
|
||||||
|
timeout_handler: ptr::null(),
|
||||||
};
|
};
|
||||||
|
|
||||||
#[cfg(all(feature = "std", unix))]
|
#[cfg(all(feature = "std", unix))]
|
||||||
impl Handler for InProcessForkExecutorGlobalData {
|
impl Handler for InProcessForkExecutorGlobalData {
|
||||||
fn handle(&mut self, signal: Signal, info: siginfo_t, context: &mut ucontext_t) {
|
fn handle(&mut self, signal: Signal, info: siginfo_t, context: &mut ucontext_t) {
|
||||||
match signal {
|
match signal {
|
||||||
Signal::SigUser2 | Signal::SigAlarm => (),
|
Signal::SigUser2 | Signal::SigAlarm => unsafe {
|
||||||
|
if !FORK_EXECUTOR_GLOBAL_DATA.timeout_handler.is_null() {
|
||||||
|
let func: ForkHandlerFuncPtr =
|
||||||
|
transmute(FORK_EXECUTOR_GLOBAL_DATA.timeout_handler);
|
||||||
|
(func)(signal, info, context, &mut FORK_EXECUTOR_GLOBAL_DATA);
|
||||||
|
}
|
||||||
|
},
|
||||||
_ => unsafe {
|
_ => unsafe {
|
||||||
if !FORK_EXECUTOR_GLOBAL_DATA.crash_handler.is_null() {
|
if !FORK_EXECUTOR_GLOBAL_DATA.crash_handler.is_null() {
|
||||||
let func: ForkHandlerFuncPtr =
|
let func: ForkHandlerFuncPtr =
|
||||||
@ -1404,6 +1441,23 @@ where
|
|||||||
phantom: PhantomData<(I, S)>,
|
phantom: PhantomData<(I, S)>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Timeout executor for [`InProcessForkExecutor`]
|
||||||
|
#[cfg(all(feature = "std", target_os = "linux"))]
|
||||||
|
pub struct TimeoutInProcessForkExecutor<'a, H, I, OT, S, SP>
|
||||||
|
where
|
||||||
|
H: FnMut(&I) -> ExitKind + ?Sized,
|
||||||
|
I: Input,
|
||||||
|
OT: ObserversTuple<I, S>,
|
||||||
|
SP: ShMemProvider,
|
||||||
|
{
|
||||||
|
harness_fn: &'a mut H,
|
||||||
|
shmem_provider: SP,
|
||||||
|
observers: OT,
|
||||||
|
handlers: InChildProcessHandlers,
|
||||||
|
itimerspec: libc::itimerspec,
|
||||||
|
phantom: PhantomData<(I, S)>,
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(all(feature = "std", unix))]
|
#[cfg(all(feature = "std", unix))]
|
||||||
impl<'a, H, I, OT, S, SP> Debug for InProcessForkExecutor<'a, H, I, OT, S, SP>
|
impl<'a, H, I, OT, S, SP> Debug for InProcessForkExecutor<'a, H, I, OT, S, SP>
|
||||||
where
|
where
|
||||||
@ -1420,6 +1474,23 @@ where
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(all(feature = "std", target_os = "linux"))]
|
||||||
|
impl<'a, H, I, OT, S, SP> Debug for TimeoutInProcessForkExecutor<'a, H, I, OT, S, SP>
|
||||||
|
where
|
||||||
|
H: FnMut(&I) -> ExitKind + ?Sized,
|
||||||
|
I: Input,
|
||||||
|
OT: ObserversTuple<I, S>,
|
||||||
|
SP: ShMemProvider,
|
||||||
|
{
|
||||||
|
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||||
|
f.debug_struct("TimeoutInProcessForkExecutor")
|
||||||
|
.field("observers", &self.observers)
|
||||||
|
.field("shmem_provider", &self.shmem_provider)
|
||||||
|
.field("itimerspec", &self.itimerspec)
|
||||||
|
.finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(all(feature = "std", unix))]
|
#[cfg(all(feature = "std", unix))]
|
||||||
impl<'a, EM, H, I, OT, S, SP, Z> Executor<EM, I, S, Z>
|
impl<'a, EM, H, I, OT, S, SP, Z> Executor<EM, I, S, Z>
|
||||||
for InProcessForkExecutor<'a, H, I, OT, S, SP>
|
for InProcessForkExecutor<'a, H, I, OT, S, SP>
|
||||||
@ -1487,6 +1558,93 @@ where
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(all(feature = "std", target_os = "linux"))]
|
||||||
|
impl<'a, EM, H, I, OT, S, SP, Z> Executor<EM, I, S, Z>
|
||||||
|
for TimeoutInProcessForkExecutor<'a, H, I, OT, S, SP>
|
||||||
|
where
|
||||||
|
H: FnMut(&I) -> ExitKind + ?Sized,
|
||||||
|
I: Input,
|
||||||
|
OT: ObserversTuple<I, S>,
|
||||||
|
SP: ShMemProvider,
|
||||||
|
{
|
||||||
|
#[allow(unreachable_code)]
|
||||||
|
#[inline]
|
||||||
|
fn run_target(
|
||||||
|
&mut self,
|
||||||
|
_fuzzer: &mut Z,
|
||||||
|
state: &mut S,
|
||||||
|
_mgr: &mut EM,
|
||||||
|
input: &I,
|
||||||
|
) -> Result<ExitKind, Error> {
|
||||||
|
unsafe {
|
||||||
|
self.shmem_provider.pre_fork()?;
|
||||||
|
match fork() {
|
||||||
|
Ok(ForkResult::Child) => {
|
||||||
|
// Child
|
||||||
|
self.shmem_provider.post_fork(true)?;
|
||||||
|
|
||||||
|
self.handlers.pre_run_target(self, state, input);
|
||||||
|
|
||||||
|
self.observers
|
||||||
|
.pre_exec_child_all(state, input)
|
||||||
|
.expect("Failed to run post_exec on observers");
|
||||||
|
|
||||||
|
let mut timerid: libc::timer_t = null_mut();
|
||||||
|
// creates a new per-process interval timer
|
||||||
|
// we can't do this from the parent, timerid is unique to each process.
|
||||||
|
libc::timer_create(libc::CLOCK_MONOTONIC, null_mut(), addr_of_mut!(timerid));
|
||||||
|
|
||||||
|
println!("Set timer! {:#?} {:#?}", self.itimerspec, timerid);
|
||||||
|
let v =
|
||||||
|
libc::timer_settime(timerid, 0, addr_of_mut!(self.itimerspec), null_mut());
|
||||||
|
println!("{:#?} {}", v, nix::errno::errno());
|
||||||
|
(self.harness_fn)(input);
|
||||||
|
|
||||||
|
self.observers
|
||||||
|
.post_exec_child_all(state, input, &ExitKind::Ok)
|
||||||
|
.expect("Failed to run post_exec on observers");
|
||||||
|
|
||||||
|
std::process::exit(0);
|
||||||
|
|
||||||
|
Ok(ExitKind::Ok)
|
||||||
|
}
|
||||||
|
Ok(ForkResult::Parent { child }) => {
|
||||||
|
// Parent
|
||||||
|
// println!("from parent {} child is {}", std::process::id(), child);
|
||||||
|
self.shmem_provider.post_fork(false)?;
|
||||||
|
|
||||||
|
let res = waitpid(child, None)?;
|
||||||
|
println!("{:#?}", res);
|
||||||
|
match res {
|
||||||
|
WaitStatus::Signaled(_, signal, _) => match signal {
|
||||||
|
nix::sys::signal::Signal::SIGALRM
|
||||||
|
| nix::sys::signal::Signal::SIGUSR2 => Ok(ExitKind::Timeout),
|
||||||
|
_ => Ok(ExitKind::Crash),
|
||||||
|
},
|
||||||
|
WaitStatus::Exited(_, code) => {
|
||||||
|
if code > 128 && code < 160 {
|
||||||
|
// Signal exit codes
|
||||||
|
let signal = code - 128;
|
||||||
|
if signal == Signal::SigAlarm as libc::c_int
|
||||||
|
|| signal == Signal::SigUser2 as libc::c_int
|
||||||
|
{
|
||||||
|
Ok(ExitKind::Timeout)
|
||||||
|
} else {
|
||||||
|
Ok(ExitKind::Crash)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Ok(ExitKind::Ok)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => Ok(ExitKind::Ok),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => Err(Error::from(e)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(all(feature = "std", unix))]
|
#[cfg(all(feature = "std", unix))]
|
||||||
impl<'a, H, I, OT, S, SP> InProcessForkExecutor<'a, H, I, OT, S, SP>
|
impl<'a, H, I, OT, S, SP> InProcessForkExecutor<'a, H, I, OT, S, SP>
|
||||||
where
|
where
|
||||||
@ -1533,6 +1691,68 @@ where
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(all(feature = "std", target_os = "linux"))]
|
||||||
|
impl<'a, H, I, OT, S, SP> TimeoutInProcessForkExecutor<'a, H, I, OT, S, SP>
|
||||||
|
where
|
||||||
|
H: FnMut(&I) -> ExitKind + ?Sized,
|
||||||
|
I: Input,
|
||||||
|
OT: ObserversTuple<I, S>,
|
||||||
|
SP: ShMemProvider,
|
||||||
|
{
|
||||||
|
/// Creates a new [`TimeoutInProcessForkExecutor`]
|
||||||
|
pub fn new<EM, OF, Z>(
|
||||||
|
harness_fn: &'a mut H,
|
||||||
|
observers: OT,
|
||||||
|
_fuzzer: &mut Z,
|
||||||
|
_state: &mut S,
|
||||||
|
_event_mgr: &mut EM,
|
||||||
|
timeout: Duration,
|
||||||
|
shmem_provider: SP,
|
||||||
|
) -> Result<Self, Error>
|
||||||
|
where
|
||||||
|
EM: EventFirer<I> + EventRestarter<S>,
|
||||||
|
OF: Feedback<I, S>,
|
||||||
|
S: HasSolutions<I> + HasClientPerfMonitor,
|
||||||
|
Z: HasObjective<I, OF, S>,
|
||||||
|
{
|
||||||
|
let handlers = InChildProcessHandlers::with_timeout::<Self, I, OT, S>()?;
|
||||||
|
let milli_sec = timeout.as_millis();
|
||||||
|
let it_value = libc::timespec {
|
||||||
|
tv_sec: (milli_sec / 1000) as _,
|
||||||
|
tv_nsec: ((milli_sec % 1000) * 1000 * 1000) as _,
|
||||||
|
};
|
||||||
|
let it_interval = libc::timespec {
|
||||||
|
tv_sec: 0,
|
||||||
|
tv_nsec: 0,
|
||||||
|
};
|
||||||
|
let itimerspec = libc::itimerspec {
|
||||||
|
it_interval,
|
||||||
|
it_value,
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
harness_fn,
|
||||||
|
shmem_provider,
|
||||||
|
observers,
|
||||||
|
handlers,
|
||||||
|
itimerspec,
|
||||||
|
phantom: PhantomData,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Retrieve the harness function.
|
||||||
|
#[inline]
|
||||||
|
pub fn harness(&self) -> &H {
|
||||||
|
self.harness_fn
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Retrieve the harness function for a mutable reference.
|
||||||
|
#[inline]
|
||||||
|
pub fn harness_mut(&mut self) -> &mut H {
|
||||||
|
self.harness_fn
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(all(feature = "std", unix))]
|
#[cfg(all(feature = "std", unix))]
|
||||||
impl<'a, H, I, OT, S, SP> HasObservers<I, OT, S> for InProcessForkExecutor<'a, H, I, OT, S, SP>
|
impl<'a, H, I, OT, S, SP> HasObservers<I, OT, S> for InProcessForkExecutor<'a, H, I, OT, S, SP>
|
||||||
where
|
where
|
||||||
@ -1552,6 +1772,26 @@ where
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(all(feature = "std", target_os = "linux"))]
|
||||||
|
impl<'a, H, I, OT, S, SP> HasObservers<I, OT, S>
|
||||||
|
for TimeoutInProcessForkExecutor<'a, H, I, OT, S, SP>
|
||||||
|
where
|
||||||
|
H: FnMut(&I) -> ExitKind + ?Sized,
|
||||||
|
I: Input,
|
||||||
|
OT: ObserversTuple<I, S>,
|
||||||
|
SP: ShMemProvider,
|
||||||
|
{
|
||||||
|
#[inline]
|
||||||
|
fn observers(&self) -> &OT {
|
||||||
|
&self.observers
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn observers_mut(&mut self) -> &mut OT {
|
||||||
|
&mut self.observers
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// signal handlers and `panic_hooks` for the child process
|
/// signal handlers and `panic_hooks` for the child process
|
||||||
#[cfg(all(feature = "std", unix))]
|
#[cfg(all(feature = "std", unix))]
|
||||||
pub mod child_signal_handlers {
|
pub mod child_signal_handlers {
|
||||||
@ -1623,6 +1863,29 @@ pub mod child_signal_handlers {
|
|||||||
|
|
||||||
libc::_exit(128 + (_signal as i32));
|
libc::_exit(128 + (_signal as i32));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(unix)]
|
||||||
|
pub(crate) unsafe fn child_timeout_handler<E, I, OT, S>(
|
||||||
|
_signal: Signal,
|
||||||
|
_info: siginfo_t,
|
||||||
|
_context: &mut ucontext_t,
|
||||||
|
data: &mut InProcessForkExecutorGlobalData,
|
||||||
|
) where
|
||||||
|
E: HasObservers<I, OT, S>,
|
||||||
|
OT: ObserversTuple<I, S>,
|
||||||
|
I: Input,
|
||||||
|
{
|
||||||
|
if data.is_valid() {
|
||||||
|
let executor = data.executor_mut::<E>();
|
||||||
|
let observers = executor.observers_mut();
|
||||||
|
let state = data.state_mut::<S>();
|
||||||
|
let input = data.take_current_input::<I>();
|
||||||
|
observers
|
||||||
|
.post_exec_child_all(state, input, &ExitKind::Timeout)
|
||||||
|
.expect("Failed to run post_exec on observers");
|
||||||
|
}
|
||||||
|
libc::_exit(128 + (_signal as i32));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
Loading…
x
Reference in New Issue
Block a user