jsdet-core 0.1.0

Core WASM-sandboxed JavaScript detonation engine
Documentation
/// Bytecode coverage tracking.
///
/// When enabled, the `QuickJS` interpreter records which bytecode offsets
/// execute. This is essential for:
///
/// - **Fuzzing feedback** — Soleno's MCTS uses coverage to guide exploration.
///   Inputs that cover new bytecodes score higher.
/// - **Detonation completeness** — Sear verifies that the payload actually
///   executed (not just parsed) by checking coverage against the script's
///   bytecode.
/// - **Evasion detection** — if a script has bytecodes that never execute
///   regardless of input, those may be dead code or environment-gated payloads.
///
/// ## How it works
///
/// 1. Host allocates a coverage bitmap in WASM linear memory via `jsdet_alloc`
/// 2. Host calls `jsdet_set_coverage(bitmap_ptr, size)` to register it
/// 3. `QuickJS` interpreter sets bits as bytecodes execute
/// 4. After execution, host reads the bitmap back
/// 5. `jsdet_count_coverage()` returns the number of executed bytecodes
/// 6. `jsdet_clear_coverage()` resets for the next run
///
/// The bitmap is bit-packed: byte N, bit M = bytecode offset (N*8 + M).
///
/// Coverage statistics from one execution.
#[derive(Debug, Clone, Default, serde::Serialize)]
pub struct CoverageReport {
    /// Total bytecode offsets covered (bits set in the bitmap).
    pub covered_offsets: u32,
    /// Size of the coverage bitmap in bytes.
    pub bitmap_size: u32,
    /// Raw bitmap data (for differential analysis between runs).
    pub bitmap: Vec<u8>,
}

impl CoverageReport {
    /// Coverage ratio: covered / (`bitmap_size` * 8).
    #[must_use]
    pub fn coverage_ratio(&self) -> f64 {
        if self.bitmap_size == 0 {
            return 0.0;
        }
        f64::from(self.covered_offsets) / (f64::from(self.bitmap_size) * 8.0)
    }

    /// Count new offsets covered compared to a previous run.
    /// This is the MCTS reward signal — new coverage = high reward.
    #[must_use]
    pub fn new_coverage_since(&self, previous: &CoverageReport) -> u32 {
        if self.bitmap.len() != previous.bitmap.len() {
            return self.covered_offsets;
        }
        let mut new_bits = 0u32;
        for (current, prev) in self.bitmap.iter().zip(previous.bitmap.iter()) {
            // Bits in current that are NOT in previous.
            let new = current & !prev;
            new_bits += new.count_ones();
        }
        new_bits
    }

    /// Merge coverage from another report (union of bitmaps).
    pub fn merge(&mut self, other: &CoverageReport) {
        if self.bitmap.len() != other.bitmap.len() {
            return;
        }
        for (a, b) in self.bitmap.iter_mut().zip(other.bitmap.iter()) {
            *a |= b;
        }
        self.covered_offsets = self.bitmap.iter().map(|b| b.count_ones()).sum();
    }
}

/// Accumulates coverage across multiple runs.
/// Used by Soleno to track total coverage during exploration.
#[derive(Debug, Clone, Default)]
pub struct CoverageAccumulator {
    merged: Vec<u8>,
    total_runs: u64,
    total_covered: u32,
}

impl CoverageAccumulator {
    #[must_use]
    pub fn new() -> Self {
        Self::default()
    }

    /// Add a new run's coverage. Returns the number of NEW offsets.
    pub fn add_run(&mut self, report: &CoverageReport) -> u32 {
        if self.merged.is_empty() {
            self.merged.clone_from(&report.bitmap);
            self.total_runs = 1;
            self.total_covered = report.covered_offsets;
            return report.covered_offsets;
        }

        if self.merged.len() != report.bitmap.len() {
            return 0;
        }

        let mut new_bits = 0u32;
        for (acc, run) in self.merged.iter_mut().zip(report.bitmap.iter()) {
            let new = run & !*acc;
            new_bits += new.count_ones();
            *acc |= run;
        }
        self.total_runs += 1;
        self.total_covered = self.merged.iter().map(|b| b.count_ones()).sum();
        new_bits
    }

    /// Total unique offsets covered across all runs.
    #[must_use]
    pub fn total_covered(&self) -> u32 {
        self.total_covered
    }

    /// Number of runs accumulated.
    #[must_use]
    pub fn total_runs(&self) -> u64 {
        self.total_runs
    }

    /// Coverage ratio.
    #[must_use]
    #[expect(clippy::cast_precision_loss)]
    pub fn coverage_ratio(&self) -> f64 {
        if self.merged.is_empty() {
            return 0.0;
        }
        f64::from(self.total_covered) / (self.merged.len() as f64 * 8.0)
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn new_coverage_detected() {
        let prev = CoverageReport {
            covered_offsets: 2,
            bitmap_size: 1,
            bitmap: vec![0b00000011],
        };
        let curr = CoverageReport {
            covered_offsets: 3,
            bitmap_size: 1,
            bitmap: vec![0b00000111],
        };
        assert_eq!(curr.new_coverage_since(&prev), 1);
    }

    #[test]
    fn accumulator_tracks_total() {
        let mut acc = CoverageAccumulator::new();
        let r1 = CoverageReport {
            covered_offsets: 2,
            bitmap_size: 1,
            bitmap: vec![0b00000011],
        };
        let new1 = acc.add_run(&r1);
        assert_eq!(new1, 2);

        let r2 = CoverageReport {
            covered_offsets: 2,
            bitmap_size: 1,
            bitmap: vec![0b00001100],
        };
        let new2 = acc.add_run(&r2);
        assert_eq!(new2, 2);
        assert_eq!(acc.total_covered(), 4);
    }

    #[test]
    fn merge_unions_bitmaps() {
        let mut a = CoverageReport {
            covered_offsets: 2,
            bitmap_size: 1,
            bitmap: vec![0b00000011],
        };
        let b = CoverageReport {
            covered_offsets: 2,
            bitmap_size: 1,
            bitmap: vec![0b00001100],
        };
        a.merge(&b);
        assert_eq!(a.covered_offsets, 4);
        assert_eq!(a.bitmap, vec![0b00001111]);
    }
}