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
47 changes: 23 additions & 24 deletions smite-scenarios/src/targets/lnd.rs
Original file line number Diff line number Diff line change
Expand Up @@ -57,25 +57,22 @@ impl LndConfig {
}
}

/// Pipes for LND coverage synchronization.
/// Pipes for the LND liveness handshake.
///
/// 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 {
/// The scenario writes a trigger byte and LND echoes it back as an ack. EOF
/// instead of the ack means LND died. `try_wait` can't replace this: a dying
/// process closes its sockets before it becomes reapable, so right after a
/// crash it still looks alive.
struct LivenessPipes {
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<()> {
impl LivenessPipes {
/// Blocks until LND acks the trigger byte. Fails if LND died.
fn check(&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(())
}
Expand All @@ -99,7 +96,7 @@ pub struct LndTarget {
lnd: ManagedProcess,
#[allow(dead_code)] // bitcoind shuts down on drop
bitcoind: ManagedProcess,
coverage_pipes: Option<CoveragePipes>,
liveness_pipes: Option<LivenessPipes>,
pubkey: secp256k1::PublicKey,
addr: SocketAddr,
bitcoin_cli: BitcoinCli,
Expand All @@ -108,12 +105,12 @@ pub struct LndTarget {
}

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

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

// Set up coverage pipes if in fuzzing mode. We keep all four pipe ends alive
// Set up liveness 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()?;
Expand Down Expand Up @@ -206,10 +203,10 @@ impl LndTarget {
None
};

let lnd = ManagedProcess::spawn(&mut cmd, "lnd")?;
let mut lnd = ManagedProcess::spawn(&mut cmd, "lnd")?;

// Extract parent-side pipe ends; child-side ends are dropped (closed) here
let coverage_pipes = pipe_ends.map(|(_, trigger_write, ack_read, _)| CoveragePipes {
let liveness_pipes = pipe_ends.map(|(_, trigger_write, ack_read, _)| LivenessPipes {
trigger_write,
ack_read,
});
Expand All @@ -218,10 +215,13 @@ impl LndTarget {
// 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, liveness_pipes, pubkey));
}
log::debug!(
"lnd not yet synced (blockheight={blockheight}, synced_to_chain={synced_to_chain})"
Expand Down Expand Up @@ -287,15 +287,15 @@ 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, liveness_pipes, 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,
liveness_pipes,
pubkey,
addr,
bitcoin_cli,
Expand All @@ -320,9 +320,8 @@ 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)?;
if let Some(pipes) = &mut self.liveness_pipes {
pipes.check().map_err(|_| TargetError::Crashed)?;
} else {
// No pipes (local mode) - just check process is running
if !self.lnd.is_running() {
Expand Down
7 changes: 5 additions & 2 deletions workloads/lnd/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -57,9 +57,12 @@ RUN wget https://bitcoincore.org/bin/bitcoin-core-${BITCOIN_VERSION}/bitcoin-${B
WORKDIR /lnd
RUN cd cmd/lncli && go build

# Copy sancov.go and build LND with coverage instrumentation
# Copy sancov.go and build LND with coverage instrumentation. align.ld
# page-aligns the coverage counters so sancov.go can map them onto AFL's map.
COPY ./workloads/lnd/sancov.go /lnd/sancov.go
RUN cd cmd/lnd && CGO_ENABLED=1 go build -v -tags=libfuzzer -gcflags=all=-d=libfuzzer
COPY ./workloads/lnd/align.ld /lnd/align.ld
RUN cd cmd/lnd && CGO_ENABLED=1 go build -v -tags=libfuzzer -gcflags=all=-d=libfuzzer \
-ldflags="-extldflags=-Wl,-T,/lnd/align.ld"

# Copy smite workspace files and build all scenario binaries
WORKDIR /smite
Expand Down
7 changes: 7 additions & 0 deletions workloads/lnd/align.ld
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
/* Gives Go's libfuzzer counter section its own pages, so sancov.go can remap
* them onto AFL's shared memory without touching neighboring data. */
SECTIONS
{
.go.fuzzcntrs ALIGN(4096) : { *(.go.fuzzcntrs) . = ALIGN(4096); }
}
INSERT AFTER .bss;
154 changes: 54 additions & 100 deletions workloads/lnd/sancov.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,81 +3,68 @@ package lnd
/*
#cgo CFLAGS: -fPIC

#define _GNU_SOURCE
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ipc.h>
#include <sys/mman.h>
#include <sys/shm.h>
#include <unistd.h>

static uint8_t *__coverage_map = NULL;
static size_t __coverage_map_size = 0;
static int __coverage_initialized = 0;

// Track multiple counter regions (from different instrumented modules).
#define MAX_COUNTER_REGIONS 128

struct counter_region {
uint8_t *start;
uint8_t *end;
};
static void fatal(const char *msg) {
fprintf(stderr, "sancov: %s\n", msg);
exit(1);
}

static struct counter_region __counter_regions[MAX_COUNTER_REGIONS];
static size_t __num_regions = 0;
static size_t __total_counters = 0;
// Called once by Go's libfuzzer runtime with the bounds of the counter section.
//
// Remaps the counter pages onto AFL's shared memory, so instrumented code
// writes coverage straight into AFL's map. Relies on align.ld giving the
// section its own pages.
void __sanitizer_cov_8bit_counters_init(char *start, char *end) {
static int initialized = 0;
size_t size = (size_t)(end - start);

static int __init_coverage_map(void) {
if (__coverage_initialized) {
return 1;
if (getenv("AFL_DUMP_MAP_SIZE")) {
printf("%zu\n", size);
exit(0);
}

const char *shm_id_str = getenv("__AFL_SHM_ID");
if (!shm_id_str) {
printf("Warning: __AFL_SHM_ID not set, coverage tracking disabled\n");
return 0;
return; // Not fuzzing.
}
const char *map_size_str = getenv("AFL_MAP_SIZE");
if (!map_size_str) {
printf("Warning: AFL_MAP_SIZE not set, coverage tracking disabled\n");
return 0;
if (initialized) {
fatal("counters registered twice");
}
initialized = 1;

int shm_id = atoi(shm_id_str);
if (shm_id < 0) {
printf("Warning: Invalid __AFL_SHM_ID value: %s\n", shm_id_str);
return 0;
size_t page = (size_t)sysconf(_SC_PAGESIZE);
if ((uintptr_t)start % page != 0) {
fatal("counter section is not page-aligned, was LND linked with align.ld?");
}

__coverage_map = (uint8_t *)shmat(shm_id, NULL, 0);
if (__coverage_map == (void *)-1) {
printf("Warning: Failed to attach to shared memory segment %d\n", shm_id);
__coverage_map = NULL;
return 0;
int shm_id = atoi(shm_id_str);
struct shmid_ds ds;
if (shmctl(shm_id, IPC_STAT, &ds) == -1) {
fatal("failed to stat the AFL shared memory segment");
}
if (ds.shm_segsz < size) {
fatal("AFL map is smaller than the counter section");
}

__coverage_map_size = (size_t)atoi(map_size_str);

printf("Coverage map initialized: %p (size: %zu)\n", __coverage_map,
__coverage_map_size);
__coverage_initialized = 1;
return 1;
}

// Copy all counter regions to AFL shared memory.
void sancov_copy_coverage_to_shmem(void) {
if (!__coverage_map || __num_regions == 0) {
return;
uint8_t *shm = (uint8_t *)shmat(shm_id, NULL, 0);
if (shm == (void *)-1) {
fatal("failed to attach the AFL shared memory segment");
}

size_t offset = 0;
for (size_t i = 0; i < __num_regions && offset < __coverage_map_size; ++i) {
size_t region_size = __counter_regions[i].end - __counter_regions[i].start;
size_t copy_size = region_size;
if (offset + copy_size > __coverage_map_size) {
copy_size = __coverage_map_size - offset;
}
memcpy(__coverage_map + offset, __counter_regions[i].start, copy_size);
offset += copy_size;
// Keep hits recorded before this point, then move only the counter pages.
// The rest of the segment (e.g. the scenario's map) stays where it is.
memcpy(shm, start, size);
size_t len = (size + page - 1) / page * page;
if (mremap(shm, len, len, MREMAP_MAYMOVE | MREMAP_FIXED, start) == MAP_FAILED) {
fatal("failed to remap the counters onto the AFL map");
}
}

Expand All @@ -86,37 +73,6 @@ void __sanitizer_cov_pcs_init(const uintptr_t *pcs_beg,
// PC table not used for AFL coverage.
}

// Called by Go's libfuzzer instrumentation to register coverage counters.
// May be called multiple times if multiple modules are instrumented.
void __sanitizer_cov_8bit_counters_init(char *start, char *end) {
const char *dump_map_size_str = getenv("AFL_DUMP_MAP_SIZE");
if (dump_map_size_str) {
printf("%zu\n", (size_t)(end - start));
exit(0);
}

__init_coverage_map();

if (__num_regions >= MAX_COUNTER_REGIONS) {
fprintf(stderr, "Error: Too many counter regions (max %d)\n",
MAX_COUNTER_REGIONS);
exit(1);
}
size_t region_size = end - start;
__counter_regions[__num_regions].start = (uint8_t *)start;
__counter_regions[__num_regions].end = (uint8_t *)end;
++__num_regions;
__total_counters += region_size;

printf("Registered counter region %zu: %zu counters\n", __num_regions,
region_size);

if (__total_counters > __coverage_map_size) {
printf("Warning: Total counter size (%zu) exceeds map size (%zu)\n",
__total_counters, __coverage_map_size);
}
}

// Empty stubs for comparison tracing hooks. Go's libfuzzer instrumentation
// emits calls to these, so we need to provide them to satisfy the linker.
// Marked weak so they can be overridden by real implementations if desired.
Expand Down Expand Up @@ -144,8 +100,10 @@ __attribute__((weak)) void __sanitizer_cov_trace_const_cmp4(uint32_t arg1,
__attribute__((weak)) void __sanitizer_cov_trace_const_cmp8(uint64_t arg1,
uint64_t arg2) {}

__attribute__((weak)) void __sanitizer_weak_hook_strcmp(const char *s1,
const char *s2) {}
__attribute__((weak)) void __sanitizer_weak_hook_strcmp(void *caller_pc,
const char *s1,
const char *s2,
int result) {}
*/
import "C"

Expand All @@ -154,41 +112,37 @@ import (
)

// This file provides coverage tracking for Go programs built with -d=libfuzzer.
// It integrates with AFL's shared memory coverage tracking.
// The C code above maps the coverage counters onto AFL's shared memory, so no
// per-execution work is needed to report coverage.
//
// Coverage sync is triggered via pipe IPC:
// - Scenario requests a sync by writing a byte to the trigger fd
// - Coverage loop reads from the trigger fd, copies coverage, and writes to the
// ack fd
// - Scenario reads from ack fd (synchronous handshake)
// The Go code answers the scenario's liveness handshake over pipes:
// - Scenario writes a byte to the trigger fd
// - We echo it on the ack fd
// - Scenario reads the ack; EOF means LND died

func init() {
// Only start coverage loop if we're in fuzzing mode
// Only answer the handshake if we're in fuzzing mode
if os.Getenv("__AFL_SHM_ID") == "" {
return
}

// Any scenario that starts LND as a subprocess must set FDs as follows:
// 3: read end of trigger pipe
// 4: write end of ack pipe
triggerFile := os.NewFile(uintptr(3), "coverage_trigger")
ackFile := os.NewFile(uintptr(4), "coverage_ack")
triggerFile := os.NewFile(uintptr(3), "liveness_trigger")
ackFile := os.NewFile(uintptr(4), "liveness_ack")

go func() {
defer triggerFile.Close()
defer ackFile.Close()

buf := make([]byte, 1)
for {
// Wait for request to copy coverage
_, err := triggerFile.Read(buf)
if err != nil {
return // Pipe closed, exit loop
}

C.sancov_copy_coverage_to_shmem()

// Signal that coverage has been copied
ackFile.Write(buf)
}
}()
Expand Down
Loading