From 964444b8276c4daab589a493b3037dcf0300980e Mon Sep 17 00:00:00 2001 From: Yunlong Date: Sun, 24 May 2026 10:07:01 +0000 Subject: [PATCH 1/4] Mapping hooper instread of copy --- .gitignore | 1 + Dockerfile | 8 ++++---- README.md | 13 +++++++------ 3 files changed, 12 insertions(+), 10 deletions(-) diff --git a/.gitignore b/.gitignore index 1f18cd9..abfd00b 100644 --- a/.gitignore +++ b/.gitignore @@ -14,3 +14,4 @@ results/ hopper.config custom.rule seeds/ +hopper_output*/ \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index 8530e70..83ae6cb 100644 --- a/Dockerfile +++ b/Dockerfile @@ -25,11 +25,11 @@ RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --de # echo '[source.tencent]' >> ${CARGO_HOME}/config && \ # echo 'registry = "http://mirrors.tencent.com/rust/index"' >> ${CARGO_HOME}/config -RUN mkdir -p /hopper -COPY . /hopper -WORKDIR /hopper +#RUN mkdir -p /hopper +#COPY . /hopper +#WORKDIR /hopper -RUN ./build.sh +#RUN ./build.sh # RUN mkdir /llvm # ENV PATH=/llvm/bin:$PATH diff --git a/README.md b/README.md index ca24166..31cecca 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,13 @@ To learn more about Hopper, check out our [paper](https://arxiv.org/pdf/2309.034 - Rust stable (>= 1.60), can be obtained using [rustup](https://rustup.rs/) - Clang (>= 5.0, [Install Clang](https://rust-lang.github.io/rust-bindgen/requirements.html)), [rust-bindgen](https://rust-lang.github.io/rust-bindgen/) leverages libclang to preprocess, parse, and type check C and C++ header files. +### Using Docker +You can choose to use the Dockerfile, which build the requirements and Hopper. +``` +docker build -t hopper ./ +docker run --name hopper_dev --privileged -v $(pwd):/hopper -v /path-to-lib:/fuzz -it --rm hopper /bin/bash +``` + ### Build Hopper itself ```sh ./build.sh @@ -23,12 +30,6 @@ To learn more about Hopper, check out our [paper](https://arxiv.org/pdf/2309.034 The script will create a `install` directory in hopper's root directory, then you can use `hopper`. To use the command anywhere, you can set the project directory in your PATH variable. -### Using Docker -You can choose to use the Dockerfile, which build the requirements and Hopper. -``` -docker build -t hopper ./ -docker run --name hopper_dev --privileged -v /path-to-lib:/fuzz -it --rm hopper /bin/bash -``` ## Compile library with Hopper Take `csjon` for example ([More examples](./examples/)). From 2ba257777281c064dfb58eb7ac30a99872da159a Mon Sep 17 00:00:00 2001 From: Yunlong Date: Sun, 24 May 2026 18:12:58 +0800 Subject: [PATCH 2/4] Implement network isolation for fork server and add related configuration --- hopper-core/src/config.rs | 10 ++++ hopper-core/src/execute/forkcli.rs | 21 +++++--- hopper-core/src/execute/limit.rs | 84 ++++++++++++++++++++++++++++++ 3 files changed, 108 insertions(+), 7 deletions(-) diff --git a/hopper-core/src/config.rs b/hopper-core/src/config.rs index 642f54a..8393f66 100644 --- a/hopper-core/src/config.rs +++ b/hopper-core/src/config.rs @@ -447,3 +447,13 @@ pub fn get_fast_recovery_interval() -> usize { std::env::var(FAST_RECOVERY_INTERVAL).map_or(256, |s| s.parse().unwrap_or(256)) }) } + +/// Network isolation is enabled by default via unshare(CLONE_NEWNET) for the fork server. +/// This prevents fuzzed network libraries from sending garbage requests to the real network. +/// Set HOPPER_DISABLE_NET_ISOLATE to disable network isolation. +pub static DISABLE_NET_ISOLATE_VAR: &str = "HOPPER_DISABLE_NET_ISOLATE"; +pub fn get_net_isolate() -> bool { + pub static NET_ISOLATE: OnceCell = OnceCell::new(); + *NET_ISOLATE.get_or_init(|| std::env::var(DISABLE_NET_ISOLATE_VAR).is_err()) +} + diff --git a/hopper-core/src/execute/forkcli.rs b/hopper-core/src/execute/forkcli.rs index fc1b595..f6d3ebd 100644 --- a/hopper-core/src/execute/forkcli.rs +++ b/hopper-core/src/execute/forkcli.rs @@ -57,17 +57,19 @@ impl ForkCli { crate::log!(info, "Run harness: {:?}, envs: {:?}", &harness, envs); config::create_dir_in_output_if_not_exist(config::HARNESS_WORK_DIR)?; let tmout = config.timeout_limit + 5; - Command::new(&harness) - .arg("--server") + let mut cmd = Command::new(&harness); + cmd.arg("--server") .envs(&envs) .stdout(Stdio::null()) .stderr(Stdio::null()) .current_dir(config::output_file_path(config::HARNESS_WORK_DIR)) .mem_limit(config.mem_limit) .core_limit() - .setsid() - .spawn() - .context("fail to spwan fork server in fuzzer")?; + .setsid(); + if config::get_net_isolate() { + cmd.net_isolate(); + } + cmd.spawn().context("fail to spwan fork server in fuzzer")?; crate::log!(info, "wait for acception.."); // May block here if the client doesn't exist. let (socket, _) = listener.accept()?; @@ -79,7 +81,8 @@ impl ForkCli { let num_fast_loop = config::get_fast_execute_loop(); if num_fast_loop > 1 { envs.insert(config::FAST_EXECUTE_LOOP, num_fast_loop.to_string()); - Command::new(&harness) + let mut fast_cmd = Command::new(&harness); + fast_cmd .arg("--server") .arg("--fast") .envs(&envs) @@ -88,7 +91,11 @@ impl ForkCli { .current_dir(config::output_file_path(config::HARNESS_WORK_DIR)) .mem_limit(config.mem_limit) .core_limit() - .setsid() + .setsid(); + if config::get_net_isolate() { + fast_cmd.net_isolate(); + } + fast_cmd .spawn() .context("fail to spwan fork server in fuzzer")?; // May block here if the client doesn't exist. diff --git a/hopper-core/src/execute/limit.rs b/hopper-core/src/execute/limit.rs index 8c5cd49..df304e8 100644 --- a/hopper-core/src/execute/limit.rs +++ b/hopper-core/src/execute/limit.rs @@ -8,6 +8,9 @@ pub trait SetLimit { fn core_limit(&mut self) -> &mut Self; /// Isolate the process and configure standard descriptors. fn setsid(&mut self) -> &mut Self; + /// Isolate the process into a new network namespace (Linux only). + /// Blocks all network access except AF_UNIX sockets used by IPC. + fn net_isolate(&mut self) -> &mut Self; } #[cfg(target_family = "unix")] @@ -64,6 +67,27 @@ impl SetLimit for Command { }; unsafe { self.pre_exec(func) } } + + fn net_isolate(&mut self) -> &mut Self { + #[cfg(target_os = "linux")] + { + let func = move || { + use nix::sched::{unshare, CloneFlags}; + + let flags = CloneFlags::CLONE_NEWUSER | CloneFlags::CLONE_NEWNET; + + if let Err(_e) = unshare(flags) { + return Err(std::io::Error::last_os_error()); + } + Ok(()) + }; + unsafe { self.pre_exec(func) } + } + #[cfg(not(target_os = "linux"))] + { + self + } + } } #[cfg(target_os = "windows")] @@ -81,4 +105,64 @@ impl SetLimit for Command { fn core_limit(&mut self) -> &mut Self { self } + + fn net_isolate(&mut self) -> &mut Self { + self + } +} + +#[cfg(test)] +mod tests { + use super::*; + use std::process::Stdio; + + /// Spawn `curl --connect-timeout 2 http://1.1.1.1` with or without + /// `net_isolate()`. Returns the process exit code. + /// + /// curl exit code 7 = "Failed to connect to host" (network unreachable). + /// curl exit code 0 = successful HTTP response. + #[cfg(target_os = "linux")] + fn curl_exit_code(isolate: bool) -> i32 { + let mut cmd = Command::new("curl"); + cmd.args([ + "-s", + "-o", + "/dev/null", + "-w", + "%{http_code}", + "--connect-timeout", + "2", + "http://1.1.1.1", + ]) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()); + if isolate { + cmd.net_isolate(); + } + let output = cmd.output().expect("failed to spawn curl"); + output.status.code().unwrap_or(-1) + } + + #[cfg(target_os = "linux")] + #[test] + fn test_net_isolate_blocks_tcp_connect() { + let code = curl_exit_code(true); + // curl exit code 7 = couldn't connect to host (network unreachable + // inside the isolated network namespace). + assert_eq!( + code, 7, + "net_isolate should make curl fail to connect (exit 7), got exit {code}" + ); + } + + #[cfg(target_os = "linux")] + #[test] + fn test_without_net_isolate_tcp_connect_allowed() { + let code = curl_exit_code(false); + // Without isolation curl should succeed (exit 0) — we can reach 1.1.1.1. + assert_eq!( + code, 0, + "curl should succeed without net_isolate (exit 0), got exit {code}" + ); + } } From 7d84f084778d04414926f687c23d5079462f95df Mon Sep 17 00:00:00 2001 From: Yunlong Date: Wed, 27 May 2026 08:06:26 +0000 Subject: [PATCH 3/4] Change hopper output to accept runtime variables. So it can executed parallel for saving at different output directory. --- hopper-core/src/config.rs | 15 +++++++++++++-- hopper-core/src/depot/io.rs | 4 ++-- hopper-core/src/execute/forkcli.rs | 2 +- hopper-core/src/feedback/review.rs | 2 +- hopper-harness/src/bin/hopper-fuzzer.rs | 6 +++--- hopper-harness/src/bin/hopper-harness.rs | 2 +- hopper-harness/src/bin/hopper-sanitizer.rs | 2 +- 7 files changed, 22 insertions(+), 11 deletions(-) diff --git a/hopper-core/src/config.rs b/hopper-core/src/config.rs index 8393f66..70d28a4 100644 --- a/hopper-core/src/config.rs +++ b/hopper-core/src/config.rs @@ -1,6 +1,17 @@ // --- Project setting --- pub const TASK_NAME: &str = task_env_var(); pub const OUTPUT_DIR: &str = out_dir_env_var(); + +/// Get the effective output directory. +/// Checks HOPPER_RUNTIME_OUT_DIR env var first (for multi-instance), +/// falls back to compile-time OUTPUT_DIR. +pub fn effective_output_dir() -> &'static str { + static DIR: once_cell::sync::OnceCell = once_cell::sync::OnceCell::new(); + DIR.get_or_init(|| { + std::env::var("HOPPER_RUNTIME_OUT_DIR") + .unwrap_or_else(|_| OUTPUT_DIR.to_string()) + }) +} // Use canary or not pub const USE_CANARY: bool = use_canary(); // Enable set function pointer @@ -382,7 +393,7 @@ const fn use_canary() -> bool { /// Get file path in output dir pub fn output_file_path(file: &str) -> std::path::PathBuf { - let path = std::path::PathBuf::from(OUTPUT_DIR); + let path = std::path::PathBuf::from(effective_output_dir()); path.join(file) } @@ -403,7 +414,7 @@ pub fn constraint_file_path() -> std::path::PathBuf { /// Get path in tmp directory /// fuzzer&harness is always run at `output`'s directory in shell pub fn tmp_file_path(file: &str) -> std::path::PathBuf { - let mut path = std::path::PathBuf::from(crate::config::OUTPUT_DIR); + let mut path = std::path::PathBuf::from(crate::config::effective_output_dir()); path.push(crate::config::TMP_DIR); path.push(file); path diff --git a/hopper-core/src/depot/io.rs b/hopper-core/src/depot/io.rs index c22e775..1b58333 100644 --- a/hopper-core/src/depot/io.rs +++ b/hopper-core/src/depot/io.rs @@ -120,7 +120,7 @@ impl DepotDir { /// Initilize depot's directories pub fn init_depot_dirs() -> eyre::Result<(DepotDir, DepotDir, DepotDir)> { crate::log!(info, "init depot dir.."); - let out_dir = PathBuf::from(config::OUTPUT_DIR); + let out_dir = PathBuf::from(config::effective_output_dir()); let inputs_dir = out_dir.join(config::INPUTS_DIR); let hangs_dir = out_dir.join(config::HANGS_DIR); let crashes_dir = out_dir.join(config::CRASHES_DIR); @@ -136,7 +136,7 @@ pub fn init_depot_dirs() -> eyre::Result<(DepotDir, DepotDir, DepotDir)> { /// Read program from queue pub fn read_input_in_queue(id: usize) -> eyre::Result { - let out_dir = PathBuf::from(config::OUTPUT_DIR); + let out_dir = PathBuf::from(config::effective_output_dir()); let file_name = format!("id_{id:06}"); let f = out_dir.join(config::INPUTS_DIR).join(file_name); let buf = std::fs::read_to_string(f)?; diff --git a/hopper-core/src/execute/forkcli.rs b/hopper-core/src/execute/forkcli.rs index f6d3ebd..018c09f 100644 --- a/hopper-core/src/execute/forkcli.rs +++ b/hopper-core/src/execute/forkcli.rs @@ -25,7 +25,7 @@ pub struct ForkCli { impl ForkCli { pub fn new(feedback: &Feedback) -> eyre::Result { let config = config::get_config(); - let harness = PathBuf::from(&config::OUTPUT_DIR) + let harness = PathBuf::from(config::effective_output_dir()) .join("bin") .join("hopper-harness"); let socket_path = socket_path(); diff --git a/hopper-core/src/feedback/review.rs b/hopper-core/src/feedback/review.rs index 9c3ffe5..7bc771d 100644 --- a/hopper-core/src/feedback/review.rs +++ b/hopper-core/src/feedback/review.rs @@ -88,7 +88,7 @@ pub struct CallRet { } fn review_file_path(id: usize, kind: &str) -> std::path::PathBuf { - let mut path = std::path::PathBuf::from(crate::config::OUTPUT_DIR); + let mut path = std::path::PathBuf::from(crate::config::effective_output_dir()); path.push(crate::config::REVIEW_DIR); path.push(format!("{id}_{kind}")); path diff --git a/hopper-harness/src/bin/hopper-fuzzer.rs b/hopper-harness/src/bin/hopper-fuzzer.rs index ea993d0..9a31c98 100644 --- a/hopper-harness/src/bin/hopper-fuzzer.rs +++ b/hopper-harness/src/bin/hopper-fuzzer.rs @@ -6,7 +6,7 @@ fn init_logger() { use flexi_logger::*; let output_file = FileSpec::default() - .directory(hopper::OUTPUT_DIR) + .directory(hopper::effective_output_dir()) .basename("fuzzer"); #[cfg(not(feature = "verbose"))] @@ -31,7 +31,7 @@ fn init_logger() { let status_writer = Box::new( FileLogWriter::builder( FileSpec::default() - .directory(hopper::OUTPUT_DIR) + .directory(hopper::effective_output_dir()) .suppress_timestamp() .basename("status"), ).rotate( @@ -45,7 +45,7 @@ fn init_logger() { let status_oneshot_writer = Box::new( FileLogWriter::builder( FileSpec::default() - .directory(hopper::OUTPUT_DIR) + .directory(hopper::effective_output_dir()) .suppress_timestamp() .basename("status_oneshot"), ) diff --git a/hopper-harness/src/bin/hopper-harness.rs b/hopper-harness/src/bin/hopper-harness.rs index 549cf90..ce5349c 100644 --- a/hopper-harness/src/bin/hopper-harness.rs +++ b/hopper-harness/src/bin/hopper-harness.rs @@ -13,7 +13,7 @@ use std::path::Path; fn init_logger(name: &str) { use flexi_logger::*; let mut output_file = FileSpec::default().basename(name); - output_file = output_file.directory(hopper::OUTPUT_DIR); + output_file = output_file.directory(hopper::effective_output_dir()); Logger::try_with_env_or_str("info") .unwrap() // Write all error, warn, and info messages .log_to_file(output_file) diff --git a/hopper-harness/src/bin/hopper-sanitizer.rs b/hopper-harness/src/bin/hopper-sanitizer.rs index 82103a3..f712b6e 100644 --- a/hopper-harness/src/bin/hopper-sanitizer.rs +++ b/hopper-harness/src/bin/hopper-sanitizer.rs @@ -87,7 +87,7 @@ fn sanitize_crash_by_clang_sanitizer_pc(crashes: Vec) -> eyre::Result Date: Sat, 30 May 2026 16:50:29 +0000 Subject: [PATCH 4/4] Implement network isolation in forked processes to prevent access to cov replay --- hopper-core/src/execute/executor.rs | 5 ++ hopper-core/src/execute/limit.rs | 94 ++++++++++++++++++++++++++--- 2 files changed, 91 insertions(+), 8 deletions(-) diff --git a/hopper-core/src/execute/executor.rs b/hopper-core/src/execute/executor.rs index 5261549..080c2a5 100644 --- a/hopper-core/src/execute/executor.rs +++ b/hopper-core/src/execute/executor.rs @@ -153,6 +153,11 @@ impl Executor { "fork time: {} micro seconds", start_at.elapsed().as_micros() ); + // Isolate the child into a new network namespace so that + // fuzzed code cannot reach the real network. + if let Err(e) = super::limit::apply_net_isolate() { + crate::log!(error, "failed to apply net isolation in child: {}", e); + } let ret = Self::execute_fn(fun); // return special signal if meet some error if let Err(e) = ret { diff --git a/hopper-core/src/execute/limit.rs b/hopper-core/src/execute/limit.rs index df304e8..c6ef840 100644 --- a/hopper-core/src/execute/limit.rs +++ b/hopper-core/src/execute/limit.rs @@ -1,5 +1,25 @@ //! Limitation of memory and time +/// Apply network isolation to the current process by creating a new +/// network namespace (Linux only). This blocks all network access +/// except AF_UNIX sockets used by IPC. +/// +/// Should be called in a forked child before executing fuzzed code. +#[cfg(target_os = "linux")] +pub fn apply_net_isolate() -> Result<(), std::io::Error> { + use nix::sched::{unshare, CloneFlags}; + let flags = CloneFlags::CLONE_NEWUSER | CloneFlags::CLONE_NEWNET; + if let Err(_e) = unshare(flags) { + return Err(std::io::Error::last_os_error()); + } + Ok(()) +} + +#[cfg(not(target_os = "linux"))] +pub fn apply_net_isolate() -> Result<(), std::io::Error> { + Ok(()) +} + pub trait SetLimit { /// Limit memory fn mem_limit(&mut self, size: Option) -> &mut Self; @@ -72,14 +92,7 @@ impl SetLimit for Command { #[cfg(target_os = "linux")] { let func = move || { - use nix::sched::{unshare, CloneFlags}; - - let flags = CloneFlags::CLONE_NEWUSER | CloneFlags::CLONE_NEWNET; - - if let Err(_e) = unshare(flags) { - return Err(std::io::Error::last_os_error()); - } - Ok(()) + apply_net_isolate() }; unsafe { self.pre_exec(func) } } @@ -116,6 +129,71 @@ mod tests { use super::*; use std::process::Stdio; + /// Fork a child, apply `apply_net_isolate()` in it, then try + /// `curl --connect-timeout 2 http://1.1.1.1`. Returns the + /// child's exit code. + #[cfg(target_os = "linux")] + fn curl_exit_code_after_fork_isolate(isolate: bool) -> i32 { + use std::process::Command; + match unsafe { nix::unistd::fork() } { + Ok(nix::unistd::ForkResult::Parent { child }) => { + use nix::sys::wait::waitpid; + match waitpid(child, None) { + Ok(nix::sys::wait::WaitStatus::Exited(_, code)) => code, + Ok(nix::sys::wait::WaitStatus::Signaled(_, _, _)) => -1, + _ => -2, + } + } + Ok(nix::unistd::ForkResult::Child) => { + if isolate { + if apply_net_isolate().is_err() { + std::process::exit(99); + } + } + let output = Command::new("curl") + .args([ + "-s", + "-o", + "/dev/null", + "-w", + "%{http_code}", + "--connect-timeout", + "2", + "http://1.1.1.1", + ]) + .output(); + let code = match output { + Ok(o) => o.status.code().unwrap_or(-1), + Err(_) => -1, + }; + std::process::exit(code); + } + Err(_) => -3, + } + } + + #[cfg(target_os = "linux")] + #[test] + fn test_apply_net_isolate_blocks_tcp_in_forked_child() { + let code = curl_exit_code_after_fork_isolate(true); + // curl exit code 7 = couldn't connect to host (network unreachable + // inside the isolated network namespace). + assert_eq!( + code, 7, + "apply_net_isolate in forked child should make curl fail to connect (exit 7), got exit {code}" + ); + } + + #[cfg(target_os = "linux")] + #[test] + fn test_without_fork_isolate_tcp_connect_allowed() { + let code = curl_exit_code_after_fork_isolate(false); + assert_eq!( + code, 0, + "curl should succeed without isolation (exit 0), got exit {code}" + ); + } + /// Spawn `curl --connect-timeout 2 http://1.1.1.1` with or without /// `net_isolate()`. Returns the process exit code. ///