Ignored qemu fuzzer for non-linux (#397)
* ignored qemu fuzzer for non-linux * fixed cfg * ignore rm -rf errors in make short_test (fuck you macos) Co-authored-by: Andrea Fioraldi <andreafioraldi@gmail.com>
This commit is contained in:
parent
ca767752d0
commit
a0ce4cfd68
@ -44,5 +44,5 @@ test: all
|
|||||||
mkdir in || true
|
mkdir in || true
|
||||||
echo a > in/a
|
echo a > in/a
|
||||||
(timeout 60s ./$(FUZZER_NAME) out in || [ $$? -eq 124 ])
|
(timeout 60s ./$(FUZZER_NAME) out in || [ $$? -eq 124 ])
|
||||||
rm -rf out
|
rm -rf out || true
|
||||||
rm -rf in
|
rm -rf in || true
|
||||||
|
@ -37,12 +37,12 @@ short_test: all
|
|||||||
echo a > in/a
|
echo a > in/a
|
||||||
# Allow sigterm as exit code
|
# Allow sigterm as exit code
|
||||||
(timeout 11s ./$(FUZZER_NAME) out in || [ $$? -eq 124 ])
|
(timeout 11s ./$(FUZZER_NAME) out in || [ $$? -eq 124 ])
|
||||||
rm -rf out
|
rm -rf out || true
|
||||||
rm -rf in
|
rm -rf in || true
|
||||||
|
|
||||||
test: all
|
test: all
|
||||||
mkdir in || true
|
mkdir in || true
|
||||||
echo a > in/a
|
echo a > in/a
|
||||||
(timeout 60s ./$(FUZZER_NAME) out in || [ $$? -eq 124 ])
|
(timeout 60s ./$(FUZZER_NAME) out in || [ $$? -eq 124 ])
|
||||||
rm -rf out
|
rm -rf out || true
|
||||||
rm -rf in
|
rm -rf in || true
|
||||||
|
203
fuzzers/qemu_launcher/src/fuzzer.rs
Normal file
203
fuzzers/qemu_launcher/src/fuzzer.rs
Normal file
@ -0,0 +1,203 @@
|
|||||||
|
//! A libfuzzer-like fuzzer using qemu for binary-only coverage
|
||||||
|
//!
|
||||||
|
use core::time::Duration;
|
||||||
|
use std::{env, path::PathBuf, process};
|
||||||
|
|
||||||
|
use libafl::{
|
||||||
|
bolts::{
|
||||||
|
current_nanos,
|
||||||
|
launcher::Launcher,
|
||||||
|
rands::StdRand,
|
||||||
|
shmem::{ShMemProvider, StdShMemProvider},
|
||||||
|
tuples::tuple_list,
|
||||||
|
},
|
||||||
|
corpus::{
|
||||||
|
Corpus, InMemoryCorpus, IndexesLenTimeMinimizerCorpusScheduler, OnDiskCorpus,
|
||||||
|
QueueCorpusScheduler,
|
||||||
|
},
|
||||||
|
events::EventConfig,
|
||||||
|
executors::{ExitKind, TimeoutExecutor},
|
||||||
|
feedback_or, feedback_or_fast,
|
||||||
|
feedbacks::{CrashFeedback, MapFeedbackState, MaxMapFeedback, TimeFeedback, TimeoutFeedback},
|
||||||
|
fuzzer::{Fuzzer, StdFuzzer},
|
||||||
|
inputs::{BytesInput, HasTargetBytes},
|
||||||
|
monitors::MultiMonitor,
|
||||||
|
mutators::scheduled::{havoc_mutations, StdScheduledMutator},
|
||||||
|
observers::{HitcountsMapObserver, TimeObserver, VariableMapObserver},
|
||||||
|
stages::StdMutationalStage,
|
||||||
|
state::{HasCorpus, StdState},
|
||||||
|
Error,
|
||||||
|
};
|
||||||
|
use libafl_qemu::{
|
||||||
|
edges, edges::QemuEdgeCoverageHelper, elf::EasyElf, emu, MmapPerms, QemuExecutor, Regs,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn fuzz() {
|
||||||
|
// Hardcoded parameters
|
||||||
|
let timeout = Duration::from_secs(1);
|
||||||
|
let broker_port = 1337;
|
||||||
|
let cores = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11];
|
||||||
|
let corpus_dirs = [PathBuf::from("./corpus")];
|
||||||
|
let objective_dir = PathBuf::from("./crashes");
|
||||||
|
|
||||||
|
// Initialize QEMU
|
||||||
|
let args: Vec<String> = env::args().collect();
|
||||||
|
let env: Vec<(String, String)> = env::vars().collect();
|
||||||
|
emu::init(&args, &env);
|
||||||
|
|
||||||
|
let mut elf_buffer = Vec::new();
|
||||||
|
let elf = EasyElf::from_file(emu::binary_path(), &mut elf_buffer).unwrap();
|
||||||
|
|
||||||
|
let test_one_input_ptr = elf
|
||||||
|
.resolve_symbol("LLVMFuzzerTestOneInput", emu::load_addr())
|
||||||
|
.expect("Symbol LLVMFuzzerTestOneInput not found");
|
||||||
|
println!("LLVMFuzzerTestOneInput @ {:#x}", test_one_input_ptr);
|
||||||
|
|
||||||
|
emu::set_breakpoint(test_one_input_ptr); // LLVMFuzzerTestOneInput
|
||||||
|
emu::run();
|
||||||
|
|
||||||
|
println!(
|
||||||
|
"Break at {:#x}",
|
||||||
|
emu::read_reg::<_, u64>(Regs::Rip).unwrap()
|
||||||
|
);
|
||||||
|
|
||||||
|
// Get the return address
|
||||||
|
let stack_ptr: u64 = emu::read_reg(Regs::Rsp).unwrap();
|
||||||
|
let mut ret_addr = [0u64];
|
||||||
|
emu::read_mem(stack_ptr, &mut ret_addr);
|
||||||
|
let ret_addr = ret_addr[0];
|
||||||
|
|
||||||
|
println!("Stack pointer = {:#x}", stack_ptr);
|
||||||
|
println!("Return address = {:#x}", ret_addr);
|
||||||
|
|
||||||
|
emu::remove_breakpoint(test_one_input_ptr); // LLVMFuzzerTestOneInput
|
||||||
|
emu::set_breakpoint(ret_addr); // LLVMFuzzerTestOneInput ret addr
|
||||||
|
|
||||||
|
let input_addr = emu::map_private(0, 4096, MmapPerms::ReadWrite).unwrap();
|
||||||
|
println!("Placing input at {:#x}", input_addr);
|
||||||
|
|
||||||
|
// The wrapped harness function, calling out to the LLVM-style harness
|
||||||
|
let mut harness = |input: &BytesInput| {
|
||||||
|
let target = input.target_bytes();
|
||||||
|
let mut buf = target.as_slice();
|
||||||
|
let mut len = buf.len();
|
||||||
|
if len > 4096 {
|
||||||
|
buf = &buf[0..4096];
|
||||||
|
len = 4096;
|
||||||
|
}
|
||||||
|
|
||||||
|
emu::write_mem(input_addr, buf);
|
||||||
|
|
||||||
|
emu::write_reg(Regs::Rdi, input_addr).unwrap();
|
||||||
|
emu::write_reg(Regs::Rsi, len).unwrap();
|
||||||
|
emu::write_reg(Regs::Rip, test_one_input_ptr).unwrap();
|
||||||
|
emu::write_reg(Regs::Rsp, stack_ptr).unwrap();
|
||||||
|
|
||||||
|
emu::run();
|
||||||
|
|
||||||
|
ExitKind::Ok
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut run_client = |state: Option<_>, mut mgr, _core_id| {
|
||||||
|
// Create an observation channel using the coverage map
|
||||||
|
let edges = unsafe { &mut edges::EDGES_MAP };
|
||||||
|
let edges_counter = unsafe { &mut edges::MAX_EDGES_NUM };
|
||||||
|
let edges_observer =
|
||||||
|
HitcountsMapObserver::new(VariableMapObserver::new("edges", edges, edges_counter));
|
||||||
|
|
||||||
|
// Create an observation channel to keep track of the execution time
|
||||||
|
let time_observer = TimeObserver::new("time");
|
||||||
|
|
||||||
|
// The state of the edges feedback.
|
||||||
|
let feedback_state = MapFeedbackState::with_observer(&edges_observer);
|
||||||
|
|
||||||
|
// Feedback to rate the interestingness of an input
|
||||||
|
// This one is composed by two Feedbacks in OR
|
||||||
|
let feedback = feedback_or!(
|
||||||
|
// New maximization map feedback linked to the edges observer and the feedback state
|
||||||
|
MaxMapFeedback::new_tracking(&feedback_state, &edges_observer, true, false),
|
||||||
|
// Time feedback, this one does not need a feedback state
|
||||||
|
TimeFeedback::new_with_observer(&time_observer)
|
||||||
|
);
|
||||||
|
|
||||||
|
// A feedback to choose if an input is a solution or not
|
||||||
|
let objective = feedback_or_fast!(CrashFeedback::new(), TimeoutFeedback::new());
|
||||||
|
|
||||||
|
// If not restarting, create a State from scratch
|
||||||
|
let mut state = state.unwrap_or_else(|| {
|
||||||
|
StdState::new(
|
||||||
|
// RNG
|
||||||
|
StdRand::with_seed(current_nanos()),
|
||||||
|
// Corpus that will be evolved, we keep it in memory for performance
|
||||||
|
InMemoryCorpus::new(),
|
||||||
|
// Corpus in which we store solutions (crashes in this example),
|
||||||
|
// on disk so the user can get them after stopping the fuzzer
|
||||||
|
OnDiskCorpus::new(objective_dir.clone()).unwrap(),
|
||||||
|
// States of the feedbacks.
|
||||||
|
// They are the data related to the feedbacks that you want to persist in the State.
|
||||||
|
tuple_list!(feedback_state),
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
|
// A minimization+queue policy to get testcasess from the corpus
|
||||||
|
let scheduler = IndexesLenTimeMinimizerCorpusScheduler::new(QueueCorpusScheduler::new());
|
||||||
|
|
||||||
|
// A fuzzer with feedbacks and a corpus scheduler
|
||||||
|
let mut fuzzer = StdFuzzer::new(scheduler, feedback, objective);
|
||||||
|
|
||||||
|
// Create a QEMU in-process executor
|
||||||
|
let executor = QemuExecutor::new(
|
||||||
|
&mut harness,
|
||||||
|
// The QEMU helpers define common hooks like coverage tracking hooks
|
||||||
|
tuple_list!(QemuEdgeCoverageHelper::new()),
|
||||||
|
tuple_list!(edges_observer, time_observer),
|
||||||
|
&mut fuzzer,
|
||||||
|
&mut state,
|
||||||
|
&mut mgr,
|
||||||
|
)
|
||||||
|
.expect("Failed to create QemuExecutor");
|
||||||
|
|
||||||
|
// Wrap the executor to keep track of the timeout
|
||||||
|
let mut executor = TimeoutExecutor::new(executor, timeout);
|
||||||
|
|
||||||
|
if state.corpus().count() < 1 {
|
||||||
|
state
|
||||||
|
.load_initial_inputs(&mut fuzzer, &mut executor, &mut mgr, &corpus_dirs)
|
||||||
|
.unwrap_or_else(|_| {
|
||||||
|
println!("Failed to load initial corpus at {:?}", &corpus_dirs);
|
||||||
|
process::exit(0);
|
||||||
|
});
|
||||||
|
println!("We imported {} inputs from disk.", state.corpus().count());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Setup an havoc mutator with a mutational stage
|
||||||
|
let mutator = StdScheduledMutator::new(havoc_mutations());
|
||||||
|
let mut stages = tuple_list!(StdMutationalStage::new(mutator));
|
||||||
|
|
||||||
|
fuzzer.fuzz_loop(&mut stages, &mut executor, &mut state, &mut mgr)?;
|
||||||
|
Ok(())
|
||||||
|
};
|
||||||
|
|
||||||
|
// The shared memory allocator
|
||||||
|
let shmem_provider = StdShMemProvider::new().expect("Failed to init shared memory");
|
||||||
|
|
||||||
|
// The stats reporter for the broker
|
||||||
|
let monitor = MultiMonitor::new(|s| println!("{}", s));
|
||||||
|
|
||||||
|
// Build and run a Launcher
|
||||||
|
match Launcher::builder()
|
||||||
|
.shmem_provider(shmem_provider)
|
||||||
|
.broker_port(broker_port)
|
||||||
|
.configuration(EventConfig::from_build_id())
|
||||||
|
.monitor(monitor)
|
||||||
|
.run_client(&mut run_client)
|
||||||
|
.cores(&cores)
|
||||||
|
.stdout_file(Some("/dev/null"))
|
||||||
|
.build()
|
||||||
|
.launch()
|
||||||
|
{
|
||||||
|
Ok(()) => (),
|
||||||
|
Err(Error::ShuttingDown) => println!("Fuzzing stopped by user. Good bye."),
|
||||||
|
Err(err) => panic!("Failed to run launcher: {:?}", err),
|
||||||
|
}
|
||||||
|
}
|
@ -1,201 +1,13 @@
|
|||||||
use core::time::Duration;
|
//! A libfuzzer-like fuzzer using qemu for binary-only coverage
|
||||||
use std::{env, path::PathBuf, process};
|
#[cfg(target_os = "linux")]
|
||||||
|
mod fuzzer;
|
||||||
use libafl::{
|
|
||||||
bolts::{
|
|
||||||
current_nanos,
|
|
||||||
launcher::Launcher,
|
|
||||||
rands::StdRand,
|
|
||||||
shmem::{ShMemProvider, StdShMemProvider},
|
|
||||||
tuples::tuple_list,
|
|
||||||
},
|
|
||||||
corpus::{
|
|
||||||
Corpus, InMemoryCorpus, IndexesLenTimeMinimizerCorpusScheduler, OnDiskCorpus,
|
|
||||||
QueueCorpusScheduler,
|
|
||||||
},
|
|
||||||
events::EventConfig,
|
|
||||||
executors::{ExitKind, TimeoutExecutor},
|
|
||||||
feedback_or, feedback_or_fast,
|
|
||||||
feedbacks::{CrashFeedback, MapFeedbackState, MaxMapFeedback, TimeFeedback, TimeoutFeedback},
|
|
||||||
fuzzer::{Fuzzer, StdFuzzer},
|
|
||||||
inputs::{BytesInput, HasTargetBytes},
|
|
||||||
monitors::MultiMonitor,
|
|
||||||
mutators::scheduled::{havoc_mutations, StdScheduledMutator},
|
|
||||||
observers::{HitcountsMapObserver, TimeObserver, VariableMapObserver},
|
|
||||||
stages::StdMutationalStage,
|
|
||||||
state::{HasCorpus, StdState},
|
|
||||||
Error,
|
|
||||||
};
|
|
||||||
use libafl_qemu::{
|
|
||||||
edges, edges::QemuEdgeCoverageHelper, elf::EasyElf, emu, MmapPerms, QemuExecutor, Regs,
|
|
||||||
};
|
|
||||||
|
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
pub fn main() {
|
pub fn main() {
|
||||||
// Hardcoded parameters
|
fuzzer::fuzz();
|
||||||
let timeout = Duration::from_secs(1);
|
}
|
||||||
let broker_port = 1337;
|
|
||||||
let cores = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11];
|
#[cfg(not(target_os = "linux"))]
|
||||||
let corpus_dirs = [PathBuf::from("./corpus")];
|
pub fn main() {
|
||||||
let objective_dir = PathBuf::from("./crashes");
|
panic!("qemu-user and libafl_qemu is only supported on linux!");
|
||||||
|
|
||||||
// Initialize QEMU
|
|
||||||
let args: Vec<String> = env::args().collect();
|
|
||||||
let env: Vec<(String, String)> = env::vars().collect();
|
|
||||||
emu::init(&args, &env);
|
|
||||||
|
|
||||||
let mut elf_buffer = Vec::new();
|
|
||||||
let elf = EasyElf::from_file(emu::binary_path(), &mut elf_buffer).unwrap();
|
|
||||||
|
|
||||||
let test_one_input_ptr = elf
|
|
||||||
.resolve_symbol("LLVMFuzzerTestOneInput", emu::load_addr())
|
|
||||||
.expect("Symbol LLVMFuzzerTestOneInput not found");
|
|
||||||
println!("LLVMFuzzerTestOneInput @ {:#x}", test_one_input_ptr);
|
|
||||||
|
|
||||||
emu::set_breakpoint(test_one_input_ptr); // LLVMFuzzerTestOneInput
|
|
||||||
emu::run();
|
|
||||||
|
|
||||||
println!(
|
|
||||||
"Break at {:#x}",
|
|
||||||
emu::read_reg::<_, u64>(Regs::Rip).unwrap()
|
|
||||||
);
|
|
||||||
|
|
||||||
// Get the return address
|
|
||||||
let stack_ptr: u64 = emu::read_reg(Regs::Rsp).unwrap();
|
|
||||||
let mut ret_addr = [0u64];
|
|
||||||
emu::read_mem(stack_ptr, &mut ret_addr);
|
|
||||||
let ret_addr = ret_addr[0];
|
|
||||||
|
|
||||||
println!("Stack pointer = {:#x}", stack_ptr);
|
|
||||||
println!("Return address = {:#x}", ret_addr);
|
|
||||||
|
|
||||||
emu::remove_breakpoint(test_one_input_ptr); // LLVMFuzzerTestOneInput
|
|
||||||
emu::set_breakpoint(ret_addr); // LLVMFuzzerTestOneInput ret addr
|
|
||||||
|
|
||||||
let input_addr = emu::map_private(0, 4096, MmapPerms::ReadWrite).unwrap();
|
|
||||||
println!("Placing input at {:#x}", input_addr);
|
|
||||||
|
|
||||||
// The wrapped harness function, calling out to the LLVM-style harness
|
|
||||||
let mut harness = |input: &BytesInput| {
|
|
||||||
let target = input.target_bytes();
|
|
||||||
let mut buf = target.as_slice();
|
|
||||||
let mut len = buf.len();
|
|
||||||
if len > 4096 {
|
|
||||||
buf = &buf[0..4096];
|
|
||||||
len = 4096;
|
|
||||||
}
|
|
||||||
|
|
||||||
emu::write_mem(input_addr, buf);
|
|
||||||
|
|
||||||
emu::write_reg(Regs::Rdi, input_addr).unwrap();
|
|
||||||
emu::write_reg(Regs::Rsi, len).unwrap();
|
|
||||||
emu::write_reg(Regs::Rip, test_one_input_ptr).unwrap();
|
|
||||||
emu::write_reg(Regs::Rsp, stack_ptr).unwrap();
|
|
||||||
|
|
||||||
emu::run();
|
|
||||||
|
|
||||||
ExitKind::Ok
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut run_client = |state: Option<_>, mut mgr, _core_id| {
|
|
||||||
// Create an observation channel using the coverage map
|
|
||||||
let edges = unsafe { &mut edges::EDGES_MAP };
|
|
||||||
let edges_counter = unsafe { &mut edges::MAX_EDGES_NUM };
|
|
||||||
let edges_observer =
|
|
||||||
HitcountsMapObserver::new(VariableMapObserver::new("edges", edges, edges_counter));
|
|
||||||
|
|
||||||
// Create an observation channel to keep track of the execution time
|
|
||||||
let time_observer = TimeObserver::new("time");
|
|
||||||
|
|
||||||
// The state of the edges feedback.
|
|
||||||
let feedback_state = MapFeedbackState::with_observer(&edges_observer);
|
|
||||||
|
|
||||||
// Feedback to rate the interestingness of an input
|
|
||||||
// This one is composed by two Feedbacks in OR
|
|
||||||
let feedback = feedback_or!(
|
|
||||||
// New maximization map feedback linked to the edges observer and the feedback state
|
|
||||||
MaxMapFeedback::new_tracking(&feedback_state, &edges_observer, true, false),
|
|
||||||
// Time feedback, this one does not need a feedback state
|
|
||||||
TimeFeedback::new_with_observer(&time_observer)
|
|
||||||
);
|
|
||||||
|
|
||||||
// A feedback to choose if an input is a solution or not
|
|
||||||
let objective = feedback_or_fast!(CrashFeedback::new(), TimeoutFeedback::new());
|
|
||||||
|
|
||||||
// If not restarting, create a State from scratch
|
|
||||||
let mut state = state.unwrap_or_else(|| {
|
|
||||||
StdState::new(
|
|
||||||
// RNG
|
|
||||||
StdRand::with_seed(current_nanos()),
|
|
||||||
// Corpus that will be evolved, we keep it in memory for performance
|
|
||||||
InMemoryCorpus::new(),
|
|
||||||
// Corpus in which we store solutions (crashes in this example),
|
|
||||||
// on disk so the user can get them after stopping the fuzzer
|
|
||||||
OnDiskCorpus::new(objective_dir.clone()).unwrap(),
|
|
||||||
// States of the feedbacks.
|
|
||||||
// They are the data related to the feedbacks that you want to persist in the State.
|
|
||||||
tuple_list!(feedback_state),
|
|
||||||
)
|
|
||||||
});
|
|
||||||
|
|
||||||
// A minimization+queue policy to get testcasess from the corpus
|
|
||||||
let scheduler = IndexesLenTimeMinimizerCorpusScheduler::new(QueueCorpusScheduler::new());
|
|
||||||
|
|
||||||
// A fuzzer with feedbacks and a corpus scheduler
|
|
||||||
let mut fuzzer = StdFuzzer::new(scheduler, feedback, objective);
|
|
||||||
|
|
||||||
// Create a QEMU in-process executor
|
|
||||||
let executor = QemuExecutor::new(
|
|
||||||
&mut harness,
|
|
||||||
// The QEMU helpers define common hooks like coverage tracking hooks
|
|
||||||
tuple_list!(QemuEdgeCoverageHelper::new()),
|
|
||||||
tuple_list!(edges_observer, time_observer),
|
|
||||||
&mut fuzzer,
|
|
||||||
&mut state,
|
|
||||||
&mut mgr,
|
|
||||||
)
|
|
||||||
.expect("Failed to create QemuExecutor");
|
|
||||||
|
|
||||||
// Wrap the executor to keep track of the timeout
|
|
||||||
let mut executor = TimeoutExecutor::new(executor, timeout);
|
|
||||||
|
|
||||||
if state.corpus().count() < 1 {
|
|
||||||
state
|
|
||||||
.load_initial_inputs(&mut fuzzer, &mut executor, &mut mgr, &corpus_dirs)
|
|
||||||
.unwrap_or_else(|_| {
|
|
||||||
println!("Failed to load initial corpus at {:?}", &corpus_dirs);
|
|
||||||
process::exit(0);
|
|
||||||
});
|
|
||||||
println!("We imported {} inputs from disk.", state.corpus().count());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Setup an havoc mutator with a mutational stage
|
|
||||||
let mutator = StdScheduledMutator::new(havoc_mutations());
|
|
||||||
let mut stages = tuple_list!(StdMutationalStage::new(mutator));
|
|
||||||
|
|
||||||
fuzzer.fuzz_loop(&mut stages, &mut executor, &mut state, &mut mgr)?;
|
|
||||||
Ok(())
|
|
||||||
};
|
|
||||||
|
|
||||||
// The shared memory allocator
|
|
||||||
let shmem_provider = StdShMemProvider::new().expect("Failed to init shared memory");
|
|
||||||
|
|
||||||
// The stats reporter for the broker
|
|
||||||
let monitor = MultiMonitor::new(|s| println!("{}", s));
|
|
||||||
|
|
||||||
// Build and run a Launcher
|
|
||||||
match Launcher::builder()
|
|
||||||
.shmem_provider(shmem_provider)
|
|
||||||
.broker_port(broker_port)
|
|
||||||
.configuration(EventConfig::from_build_id())
|
|
||||||
.monitor(monitor)
|
|
||||||
.run_client(&mut run_client)
|
|
||||||
.cores(&cores)
|
|
||||||
.stdout_file(Some("/dev/null"))
|
|
||||||
.build()
|
|
||||||
.launch()
|
|
||||||
{
|
|
||||||
Ok(()) => (),
|
|
||||||
Err(Error::ShuttingDown) => println!("Fuzzing stopped by user. Good bye."),
|
|
||||||
Err(err) => panic!("Failed to run launcher: {:?}", err),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user