Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,4 @@ results/
hopper.config
custom.rule
seeds/
hopper_output*/
8 changes: 4 additions & 4 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
13 changes: 7 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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/)).
Expand Down
25 changes: 23 additions & 2 deletions hopper-core/src/config.rs
Original file line number Diff line number Diff line change
@@ -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<String> = 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
Expand Down Expand Up @@ -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)
}

Expand All @@ -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
Expand Down Expand Up @@ -447,3 +458,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<bool> = OnceCell::new();
*NET_ISOLATE.get_or_init(|| std::env::var(DISABLE_NET_ISOLATE_VAR).is_err())
}

4 changes: 2 additions & 2 deletions hopper-core/src/depot/io.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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<FuzzProgram> {
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)?;
Expand Down
5 changes: 5 additions & 0 deletions hopper-core/src/execute/executor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
23 changes: 15 additions & 8 deletions hopper-core/src/execute/forkcli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ pub struct ForkCli {
impl ForkCli {
pub fn new(feedback: &Feedback) -> eyre::Result<Self> {
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();
Expand Down Expand Up @@ -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()?;
Expand All @@ -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)
Expand All @@ -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.
Expand Down
162 changes: 162 additions & 0 deletions hopper-core/src/execute/limit.rs
Original file line number Diff line number Diff line change
@@ -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<u64>) -> &mut Self;
Expand All @@ -8,6 +28,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")]
Expand Down Expand Up @@ -64,6 +87,20 @@ impl SetLimit for Command {
};
unsafe { self.pre_exec(func) }
}

fn net_isolate(&mut self) -> &mut Self {
#[cfg(target_os = "linux")]
{
let func = move || {
apply_net_isolate()
};
unsafe { self.pre_exec(func) }
}
#[cfg(not(target_os = "linux"))]
{
self
}
}
}

#[cfg(target_os = "windows")]
Expand All @@ -81,4 +118,129 @@ 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;

/// 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.
///
/// 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}"
);
}
}
2 changes: 1 addition & 1 deletion hopper-core/src/feedback/review.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading