diff --git a/smite-scenarios/src/targets/lnd.rs b/smite-scenarios/src/targets/lnd.rs index d6613562..1e861286 100644 --- a/smite-scenarios/src/targets/lnd.rs +++ b/smite-scenarios/src/targets/lnd.rs @@ -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(()) } @@ -99,7 +96,7 @@ pub struct LndTarget { lnd: ManagedProcess, #[allow(dead_code)] // bitcoind shuts down on drop bitcoind: ManagedProcess, - coverage_pipes: Option, + liveness_pipes: Option, pubkey: secp256k1::PublicKey, addr: SocketAddr, bitcoin_cli: BitcoinCli, @@ -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, secp256k1::PublicKey), TargetError> { + ) -> Result<(ManagedProcess, Option, secp256k1::PublicKey), TargetError> { log::info!("Starting lnd..."); let lnd_dir = data_dir.join("lnd"); @@ -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()?; @@ -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, }); @@ -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})" @@ -287,7 +287,7 @@ 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"); @@ -295,7 +295,7 @@ impl Target for LndTarget { Ok(Self { lnd, bitcoind, - coverage_pipes, + liveness_pipes, pubkey, addr, bitcoin_cli, @@ -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() { diff --git a/workloads/lnd/Dockerfile b/workloads/lnd/Dockerfile index dc2671ef..4b767241 100644 --- a/workloads/lnd/Dockerfile +++ b/workloads/lnd/Dockerfile @@ -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 diff --git a/workloads/lnd/align.ld b/workloads/lnd/align.ld new file mode 100644 index 00000000..1df440f1 --- /dev/null +++ b/workloads/lnd/align.ld @@ -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; diff --git a/workloads/lnd/sancov.go b/workloads/lnd/sancov.go index dac197bd..3068640c 100644 --- a/workloads/lnd/sancov.go +++ b/workloads/lnd/sancov.go @@ -3,81 +3,68 @@ package lnd /* #cgo CFLAGS: -fPIC +#define _GNU_SOURCE #include #include #include #include -#include +#include #include +#include -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"); } } @@ -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. @@ -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" @@ -154,16 +112,16 @@ 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 } @@ -171,8 +129,8 @@ func init() { // 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() @@ -180,15 +138,11 @@ func init() { 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) } }()