Skip to content
Merged
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: 0 additions & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

11 changes: 9 additions & 2 deletions smite-nyx-sys/src/nyx-crash-handler.c
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@
/// Compile-time options:
/// - -DCATCH_SIGNALS: Install our own signal handler for fatal signals, and
/// block any attempts to override our signal handler.
/// - -DALLOW_HANDLER_OVERRIDE: With -DCATCH_SIGNALS, let the target replace our
/// handlers. For runtimes like Go that need their own and forward fatal
/// signals they don't handle to ours.
/// - -DENABLE_NYX: Use nyx hypercalls to let nyx know that a crash has occured.
/// If not set, crash reports are written to /tmp/smite-crash.log.
/// - -DASAN_LOG_PATH=<path>: Path to the ASan log file.
Expand All @@ -34,7 +37,8 @@
#include "nyx.h"
#endif

// Must match PANIC_LOG_PATH in workloads/ldk/src/main.rs.
// Must match PANIC_LOG_PATH in workloads/ldk/src/main.rs and
// workloads/lnd/sancov.go.
#define PANIC_LOG_PATH "/tmp/smite-panic.log"
#define ASAN_LOG_PATH "/tmp/asan.log"
#define MAX_CUSTOM_BACKTRACE_SIZE 50
Expand Down Expand Up @@ -72,7 +76,8 @@ void append_target_log(const char *path) {
return;
}

char buffer[0x100000];
// Static: Go calls our handler on its 32 KiB signal stack.
static char buffer[0x100000];
size_t bytes_read = fread(buffer, 1, sizeof(buffer) - 1, file);
fclose(file);

Expand Down Expand Up @@ -186,6 +191,7 @@ void __assert_perror_fail(int errnum, const char *file, unsigned int line,

#ifdef CATCH_SIGNALS

#ifndef ALLOW_HANDLER_OVERRIDE
int sigaction(int signum, const struct sigaction *act,
struct sigaction *oldact) {
int (*_sigaction)(int signum, const struct sigaction *act,
Expand All @@ -204,6 +210,7 @@ int sigaction(int signum, const struct sigaction *act,
return _sigaction(signum, act, oldact);
}
}
#endif

void fault_handler(int signo, siginfo_t *info, void *extra) {
char signal_msg[0x1000];
Expand Down
1 change: 0 additions & 1 deletion smite-scenarios/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ thiserror.workspace = true
bitcoin.workspace = true

hex = "0.4"
libc.workspace = true
serde.workspace = true
serde_json.workspace = true
tempfile = "3"
2 changes: 1 addition & 1 deletion smite-scenarios/src/scenarios/encrypted_bytes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ impl<T: Target> Scenario for EncryptedBytesScenario<T> {
log::debug!("[{:?}] Target responded with pong", start.elapsed());
}

// Check if target is still alive (and trigger coverage sync for LND)
// Check if target is still alive
if let Err(e) = self.target.check_alive() {
log::debug!("[{:?}] check_alive: {e}", start.elapsed());
return ScenarioResult::Fail(Violation::Crashed.to_string());
Expand Down
7 changes: 0 additions & 7 deletions smite-scenarios/src/targets.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,6 @@ const CRASH_LOG_PATH: &str = "/tmp/smite-crash.log";
/// In Nyx mode, crashes are reported directly via hypercall and we never get to
/// this point. In local mode, the crash handler writes crash data to a file.
///
/// Used by targets that have an external crash handler (CLN, Eclair).
///
/// # Errors
///
/// Returns [`TargetError::Crashed`] if the crash log file exists.
Expand Down Expand Up @@ -81,11 +79,6 @@ pub trait Target: Sized {

/// Check if target is still alive. Returns `Err(Crashed)` if dead.
///
/// Implementation varies by target:
/// - LND: Pipe-based coverage sync (Go can't write to AFL shm directly)
/// - CLN/LDK: Process liveness check (C/Rust AFL instrumentation writes directly)
/// - Eclair: Process liveness check (Java agent writes directly via JNI shmat)
///
/// # Errors
///
/// Returns [`TargetError::Crashed`] if the target has crashed.
Expand Down
6 changes: 3 additions & 3 deletions smite-scenarios/src/targets/cln.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
//! CLN (Core Lightning) target implementation.
//!
//! CLN is written in C, so AFL instrumentation (via `afl-clang-fast`) writes
//! directly to shared memory. No coverage pipes are needed.
//! directly to shared memory.
//!
//! CLN uses a subdaemon architecture: `lightningd` spawns separate binaries
//! (`lightning_connectd`, `lightning_gossipd`, etc.). Global subdaemons have
Expand Down Expand Up @@ -299,14 +299,14 @@ impl Drop for ClnTarget {
// where the CLI has returned but lightningd hasn't exited yet.
log::debug!("lightningd: waiting for process to exit");
let deadline = std::time::Instant::now() + Duration::from_secs(5);
while self.cln.is_running() && std::time::Instant::now() < deadline {
while !self.cln.has_exited() && std::time::Instant::now() < deadline {
std::thread::sleep(Duration::from_millis(10));
}
} else {
log::debug!("lightningd: lightning-cli stop failed, falling back to SIGTERM");
}
// ManagedProcess::drop handles cleanup. If lightningd already exited,
// is_running() returns false and no signal is sent. If the timeout
// has_exited() returns true and no signal is sent. If the timeout
// expired, ManagedProcess sends SIGTERM as a fallback and targets the
// whole process group so any lingering subdaemons are cleaned up too.
}
Expand Down
4 changes: 2 additions & 2 deletions smite-scenarios/src/targets/ldk.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
//! LDK target implementation.
//!
//! Unlike LND, LDK is written in Rust so AFL instrumentation writes directly
//! to shared memory. No coverage pipes are needed.
//! LDK is written in Rust, so AFL instrumentation writes directly to shared
//! memory.

use std::fs;
use std::io::{BufRead, BufReader};
Expand Down
126 changes: 19 additions & 107 deletions smite-scenarios/src/targets/lnd.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
//! LND target implementation.

use std::fs;
use std::io::{PipeReader, PipeWriter, Read, Write};
use std::net::SocketAddr;
use std::os::unix::io::AsRawFd;
use std::path::Path;
use std::process::{Command, Stdio};
use std::time::Duration;
Expand All @@ -14,7 +12,7 @@ use smite::bitcoin::BitcoinCli;
use smite::process::ManagedProcess;

use super::bitcoind;
use super::{Target, TargetError, TargetRpc};
use super::{Target, TargetError, TargetRpc, check_crash_log};

/// Configuration for the LND target.
pub struct LndConfig {
Expand Down Expand Up @@ -57,30 +55,6 @@ impl LndConfig {
}
}

/// Pipes for LND coverage synchronization.
///
/// Go can't write directly to AFL's shared memory, so we use pipes:
/// 1. Scenario writes trigger byte
/// 2. LND copies coverage to AFL shared memory
/// 3. LND writes ack byte
/// 4. If scenario's ack read fails (EOF), LND crashed
struct CoveragePipes {
trigger_write: PipeWriter,
ack_read: PipeReader,
}

impl CoveragePipes {
/// Triggers LND to copy coverage counters to AFL shared memory.
fn sync(&mut self) -> std::io::Result<()> {
let mut buf = [0u8; 1];
// Write 1 byte to trigger coverage copy
self.trigger_write.write_all(&buf)?;
// Wait for coverage copy to finish (EOF = crash)
self.ack_read.read_exact(&mut buf)?;
Ok(())
}
}

/// RPC handle for interacting with LND node target.
#[derive(Debug, Clone)]
pub struct LndRpc;
Expand All @@ -99,7 +73,6 @@ pub struct LndTarget {
lnd: ManagedProcess,
#[allow(dead_code)] // bitcoind shuts down on drop
bitcoind: ManagedProcess,
coverage_pipes: Option<CoveragePipes>,
pubkey: secp256k1::PublicKey,
addr: SocketAddr,
bitcoin_cli: BitcoinCli,
Expand All @@ -108,12 +81,12 @@ pub struct LndTarget {
}

impl LndTarget {
/// Starts LND and waits for it to be ready. Returns the process, coverage
/// pipes (if in fuzzing mode), and LND's identity pubkey.
/// Starts LND and waits for it to be ready. Returns the process and LND's
/// identity pubkey.
fn start_lnd(
config: &LndConfig,
data_dir: &Path,
) -> Result<(ManagedProcess, Option<CoveragePipes>, secp256k1::PublicKey), TargetError> {
) -> Result<(ManagedProcess, secp256k1::PublicKey), TargetError> {
log::info!("Starting lnd...");

let lnd_dir = data_dir.join("lnd");
Expand Down Expand Up @@ -147,81 +120,26 @@ impl LndTarget {
.stdout(Stdio::null())
.stderr(Stdio::null());

// Set up coverage pipes if in fuzzing mode. We keep all four pipe ends alive
// until after spawn so the FDs are valid when the child forks.
let pipe_ends = if std::env::var("__AFL_SHM_ID").is_ok() {
let (trigger_read, trigger_write) = std::io::pipe()?;
let (ack_read, ack_write) = std::io::pipe()?;

let trigger_fd = trigger_read.as_raw_fd();
let ack_fd = ack_write.as_raw_fd();

// SAFETY: This closure runs in the child process after fork, before exec.
// We only call async-signal-safe libc functions (fcntl, dup2, close) and
// create io::Error from last_os_error() which just stores an i32 errno.
unsafe {
use std::os::unix::process::CommandExt;

cmd.pre_exec(move || {
let mut t = trigger_fd;
let mut a = ack_fd;

// Move FDs to safe range (>= 10) to avoid conflicts with targets 3 and 4.
// For example, if ack_fd were 3, dup2(trigger_fd, 3) would close it.
if t < 10 {
let new_t = libc::fcntl(t, libc::F_DUPFD, 10);
if new_t == -1 {
return Err(std::io::Error::last_os_error());
}
libc::close(t);
t = new_t;
}
if a < 10 {
let new_a = libc::fcntl(a, libc::F_DUPFD, 10);
if new_a == -1 {
return Err(std::io::Error::last_os_error());
}
libc::close(a);
a = new_a;
}

// Assign to fixed FD numbers that LND's sancov.go expects
if libc::dup2(t, 3) == -1 {
return Err(std::io::Error::last_os_error());
}
if libc::dup2(a, 4) == -1 {
return Err(std::io::Error::last_os_error());
}

// Close the intermediate FDs (dup2 doesn't close the source)
libc::close(t);
libc::close(a);

Ok(())
});
}

Some((trigger_read, trigger_write, ack_read, ack_write))
} else {
None
};

let lnd = ManagedProcess::spawn(&mut cmd, "lnd")?;
// LD_PRELOAD the crash handler to report crashes immediately (before
// process teardown closes TCP sockets). Go only raises a signal the
// handler sees at GOTRACEBACK=crash; by default it exits with status 2.
if let Ok(handler) = std::env::var("SMITE_CRASH_HANDLER") {
cmd.env("LD_PRELOAD", handler).env("GOTRACEBACK", "crash");
}

// Extract parent-side pipe ends; child-side ends are dropped (closed) here
let coverage_pipes = pipe_ends.map(|(_, trigger_write, ack_read, _)| CoveragePipes {
trigger_write,
ack_read,
});
let mut lnd = ManagedProcess::spawn(&mut cmd, "lnd")?;

// Wait for LND to be ready and fully synced. We poll getinfo until
// block_height matches the initial blocks we generated.
log::info!("Waiting for lnd to be ready and synced...");
for _ in 0..120 {
if !lnd.is_running() {
return Err(TargetError::StartFailed("lnd exited during startup".into()));
}
if let Ok((pubkey, blockheight, synced_to_chain)) = Self::query_info(config, &lnd_dir) {
if blockheight >= bitcoind::INITIAL_BLOCKS && synced_to_chain {
log::info!("lnd synced (blockheight={blockheight})");
return Ok((lnd, coverage_pipes, pubkey));
return Ok((lnd, pubkey));
}
log::debug!(
"lnd not yet synced (blockheight={blockheight}, synced_to_chain={synced_to_chain})"
Expand Down Expand Up @@ -287,15 +205,14 @@ impl Target for LndTarget {
let (data_path, temp_dir) = bitcoind::resolve_data_dir()?;

let (bitcoind, bitcoin_cli) = bitcoind::start(&config.bitcoind_config(), &data_path)?;
let (lnd, coverage_pipes, pubkey) = Self::start_lnd(&config, &data_path)?;
let (lnd, pubkey) = Self::start_lnd(&config, &data_path)?;
let addr = SocketAddr::from(([127, 0, 0, 1], config.lnd_p2p_port));

log::info!("Both daemons are running, ready to fuzz");

Ok(Self {
lnd,
bitcoind,
coverage_pipes,
pubkey,
addr,
bitcoin_cli,
Expand All @@ -320,14 +237,9 @@ impl Target for LndTarget {
}

fn check_alive(&mut self) -> Result<(), TargetError> {
// If we have coverage pipes, sync triggers coverage copy AND detects crashes
if let Some(pipes) = &mut self.coverage_pipes {
pipes.sync().map_err(|_| TargetError::Crashed)?;
} else {
// No pipes (local mode) - just check process is running
if !self.lnd.is_running() {
return Err(TargetError::Crashed);
}
check_crash_log()?;
if !self.lnd.is_running() {
return Err(TargetError::Crashed);
}
Ok(())
}
Expand Down
Loading
Loading