Skip to content

Releases: sebastian-heinz/haunt

v0.6.0

29 Apr 01:35

Choose a tag to compare

Schema/layouts system + strict-validation hardening.
Breaking: RegName::Eflags removed; dsl::validate_field_paths now
takes &Registry. See CHANGELOG.md for the full list.

v0.5.2

28 Apr 02:06

Choose a tag to compare

Release-engineering only. No behavioural changes to the agent, CLI,
or HTTP protocol — haunt-core, haunt-windows, haunt-inject,
and haunt are byte-for-byte equivalent to v0.5.1 when built from
the same toolchain.

Changed

  • Renamed CLI release artifacts to match the project's existing
    local naming.
    v0.5.1's haunt-{linux,macos}-x64 are now
    haunt-{linux,macos}-x86_64, aligning with the Rust target-triple
    convention already used in the local dist/ directory and removing
    drift between the GitHub release page and any scripts pointing at
    local builds.
  • macOS builds collapsed to a single cli-macos job. The two
    per-arch matrix entries (macos-13 for Intel, macos-14 for Apple
    Silicon) are now one job on macos-14 that cross-compiles
    x86_64-apple-darwin from the arm64 host. Apple's clang ships both
    SDKs so this is a native cross — same toolchain, same binary. This
    eliminates the macos-13 runner-queue dependency that left v0.5.1
    stuck for ~25 min waiting on a runner allocation that never landed.

Added

  • haunt-macos-universallipo-merged fat binary that runs on
    either macOS arch. For distribution scripts that don't want to
    branch on architecture, or for users who don't know which Mac their
    end-recipient runs.

Removed

  • Per-asset *.sha256 sidecars for the Linux + macOS CLI
    binaries that v0.5.1 added. The Windows job's consolidated
    SHA256SUMS was already the only checksum file in the local
    dist/ convention; the asymmetry of "Windows binaries get a
    combined sums file, every other binary gets its own sidecar"
    was confusing without buying anything verifiable that
    shasum -a 256 <file> from the user's own host doesn't already
    give them.

v0.5.1

28 Apr 01:21

Choose a tag to compare

Added

  • haunt CLI prebuilt for Linux and macOS. The DLL and injector
    are Windows-by-construction, but the CLI is just loopback HTTP over
    std::net with no native deps — there's no reason it should only be
    available as haunt.exe. Each release page now also ships:
    • haunt-linux-x64, haunt-linux-arm64 — musl-static, run on any
      Linux distro from kernel 2.6+ with no glibc / shared-lib deps.
    • haunt-macos-x64, haunt-macos-arm64 — native Apple Silicon and
      Intel builds.
      Each artifact ships with a sibling *.sha256 (formatted by
      shasum -a 256 so a sha256sum -c from any host validates it).
      The Windows job retains its consolidated SHA256SUMS file so
      existing release-asset names are unchanged.

v0.5.0

28 Apr 01:09

Choose a tag to compare

Changed

  • Trace + log ring capacity raised from 4 096 to 40 960 records. At 4 096 a short burst from a high-rate --log BP could slide records off the front of the ring before a /events caller finished its setup round trip. The new bound matches the new limit/tail ceiling so a single call can drain the whole ring.
  • MAX_LONG_POLL_TIMEOUT_MS raised from 60 000 to 300 000 ms (5 min). Operators tailing /events or /logs over a slow link no longer have to re-poll every minute. Both the HTTP-edge validator and the platform-side wait_halt / events::poll / logs::poll impls enforce the new ceiling.
  • CLI socket read timeout raised from 90 s to 310 s. Previous value pre-dated the long-poll cap bump; --timeout values above ~90 s would have the CLI's TcpStream abort the read before the agent could respond. New value covers the 300 s agent ceiling plus loopback slack.

Added

  • MAX_TRACE_BATCH shared constant as the single source of truth for both the events / logs ring capacity and the HTTP-edge limit / tail validator. The previous duplicated 4096 literal in handle_events / handle_logs would silently cap clients to the old value if the ring grew without the validator being touched.

Documentation

  • README's Concurrency paragraph now lists /logs alongside /halts/wait and /events as endpoints sharing the 64-of-80 long-poll sub-cap (the classifier already routed /logs there; the prose was stale).
  • CLI USAGE for events and logs now spells out the --limit default (256) and max (40960 = ring size); previously users only learned the bound from a 400 response.

v0.4.0

28 Apr 01:09

Choose a tag to compare

Changed (breaking)

  • Strict-validation tightening across the HTTP surface. Per the
    threat-model note added to AGENTS.md (we share an address space with
    the host; auth is not load-bearing, correctness is), every silent
    acceptance path is now a 400:
    • Unknown query parameters are rejected by every endpoint that
      iterates query (e.g. /bp/set?halft_if=... — typo of halt_if
      used to silently set a BP with no halt gate; now 400 names the
      offending key). parse_resume_mode("foo=bar") now errors instead
      of defaulting to Continue.
    • No-arg endpoints reject any query. /ping?foo=1, /info?x=y,
      /modules?stale=1, /halts?..., /threads?..., /memory/regions?...,
      /bp/list?..., /shutdown?..., /bp/<id>?..., and
      /modules/<name>/exports?... all 400 when given any params.
    • Malformed query pairs are rejected at the dispatcher. ?foo
      (missing =) used to be silently dropped by parse_query; now
      route() 400s with malformed query pair (expected key=value): \foo``. Single source of truth — handlers don't have to recheck.
    • No more silent clamp on user-supplied counts. events/logs
      ?limit=0 was silently raised to 1; ?limit=99999 was silently
      capped to 4096; events ?tail=0 was silently raised to 1; same
      for tail upper bound. All four cases now 400 with a message that
      names the parameter and the legal range. /halts/<id>/stack?depth=0
      likewise. /halts/wait?timeout=... and /events/logs?timeout=...
      over MAX_LONG_POLL_TIMEOUT_MS (60 s) now 400 instead of being
      clamped (the platform layer still re-clamps defensively in case a
      direct caller bypasses the HTTP edge).
    • /memory/read?format= rejects unknown values. Previously
      anything other than raw was silently treated as hex; now
      format=hax 400s rather than yielding hex output.

Fixed

  • resume --ret now logs SW-BP install failure. The one-shot SW
    BP planted at [xSP] was installed via let _ = super::set(...)
    — any Conflict/Unwritable/etc. error was silently dropped and
    the user got 200 resumed while the thread ran free past the
    function with no halt. Now warn!s with the failure reason, visible
    via haunt logs.
  • reject_page_covering_sw_bp overflow. The neighbouring
    page_addr.checked_add(page_size) correctly rejected wrap, but
    end.saturating_add(ps - 1) & !(ps - 1) could wrap end_page down
    past end and silently miss a SW BP on the last covered page. Now
    uses checked_add symmetrically with page::install.
  • dsl::render no longer unwraps write! to a String. The
    unwrap was infallible in practice (fmt::Write for String never
    errors) but violated the no-unwrap policy; a future refactor that
    swapped the sink for something fallible would have turned a render
    bug into a host-process abort. Now uses let _ = write!(...).

Docs

  • AGENTS.md threat model section. Codifies that haunt's threat
    model is the host process (not network attackers): we share the
    address space, every silent default is a host bug, every panic kills
    the host. Strict validation, panic-free hot paths, no unwrap, no
    unwrap_or on user input, no silent defaults — non-negotiable.

Added

  • events --tail N returns the most recent N matching records
    in chronological order, regardless of --since. Disables long-poll
    (a snapshot, not a wait). Solves the ring-overflow foot-shape where
    --since=0 slid off the front of the deque while the caller was
    setting up. Server: ?tail=N query param; long-poll suppressed when
    set.
  • events --bp-id N server-side filter — only records from BP
    N come back, useful when several BPs fire at high rate and the
    client only cares about one. Server: ?bp_id=N query param.
  • CLI-side address annotation in haunt events / haunt logs.
    Hex sequences in record msg fields that fall inside a loaded
    module are annotated inline as 0x... (module+0xoffset) against
    /modules. Default-on; --no-annotate for stable output in
    scripts. Done CLI-side rather than VEH-side because module
    enumeration takes the loader lock — calling it from the VEH (where
    --log records are emitted) is a deadlock vector we deliberately
    avoid for the same reason resume --ret was moved off
    modules::list to VirtualQuery.

Changed

  • --if split into --log-if and --halt-if. The original
    single cond gate covered halt + log + event uniformly. Splitting
    lets a single BP log every call but halt only on a specific
    predicate (e.g. --log "..." --halt-if "[ecx] == 0x..."). Per the
    AGENTS.md no-compat policy, --if is removed; HTTP ?cond= is
    removed in favour of ?log_if= and ?halt_if=. bp list /
    bp info output now has log_if=... and halt_if=... fields in
    place of cond=.... entry.hits continues to count every fire
    regardless of either gate.

Added

  • GET /logs endpoint and haunt logs CLI for tailing the agent's
    own info! / warn! / error! output. Mirrors /events: bounded
    ring (4096), monotonic id, long-poll up to 60 s, same ?since=&limit= &timeout= query shape. Replaces the previous OutputDebugStringA
    sink, which could block the agent when a debugger was attached but
    not draining the LPC queue, mixed output across every process on the
    box, and required DebugView / a real debugger to consume. The new
    endpoint flows through the same auth / CSRF / in-flight-cap machinery
    as everything else and works over SSH the same way.

Removed

  • OutputDebugStringA log sink in haunt-windows (DebugStringSink).
    Use haunt logs to drain the agent's output instead.

Fixed

  • Step → continue from a HW BP halt no longer kills the host.
    apply_resume_mode(Step) set TRAP_FLAG; nothing cleared it on the
    subsequent Continue (HW BP path doesn't have a rearm to consume
    TF, unlike the SW / page paths). The thread resumed with TF=1, the
    next instruction TF-trapped, on_single_step found no rearm / no
    STEP::Step / no DR slot fired, fell through to EXCEPTION_CONTINUE_ SEARCH, and the OS unhandled-exception filter terminated the host.
    Reachable from the routine "halt at HW BP, single-step a few times,
    continue" workflow. Fixed by clearing TF unconditionally at entry to
    on_single_step (with apply_resume_mode(Step) re-setting it as the
    only legitimate post-handler use). The same bug bit any "step ...
    step ... continue" sequence, not just HW BPs.
  • Multi-page accesses no longer lose page-BP rearms.
    PENDING_PAGE was a single Cell<Option<PageRearm>>. A misaligned
    load crossing a page boundary, rep movs over multiple pages, or
    any instruction that traps on more than one guarded page in
    succession would overwrite the earlier rearm — silently turning the
    BP one-shot for every page after the first on the very first multi-
    page hit. Now a Cell<Vec<PageRearm>> (capped at 64 entries; uses
    Cell::take / Cell::set rather than RefCell to stay panic-free).
  • Failed page-BP installs no longer leak PAGE_GUARD orphans.
    Two paths produced orphan-guarded pages with no registry entry,
    killing the host on the next access: (1) query_protect failure
    mid-loop bailed via ? with no rollback, leaking guards on every
    already-protected page; (2) set_protect failure rolled back, but
    if a rollback VirtualProtect itself failed the page stayed
    guarded. Both paths now go through one rollback that records any
    unreversible page in ORPHAN_PAGES. The VEH consults the set on a
    guard fault that doesn't match any registered BP and returns
    EXCEPTION_CONTINUE_EXECUTION instead of propagating to a host
    kill. Capped at 4 K entries with arbitrary eviction so a degenerate
    workflow can't grow the set unboundedly.
  • SW BP install rejects pages that already have PAGE_GUARD set.
    write_byte's VirtualProtect(PAGE_EXECUTE_READWRITE) is page-
    granular and silently strips PAGE_GUARD for the duration of the
    byte write — defeating the OS stack-growth guard, AV sentinels, JIT
    runtime traps, and foreign debuggers using PAGE_GUARD. Now
    VirtualQuery-checked at install time and rejected with Conflict
    / 409. Symmetric with reject_sw_overlapping_page_bp (which
    catches haunt's own page BPs); this catches third-party guards.
  • clear() racing an in-flight SW BP hit no longer kills the host.
    clear() could restore the original byte and remove the registry
    entry between the int3 firing and on_int3 acquiring the registry
    lock. The CPU's saved IP points past the int3 byte (int3 is a trap),
    so resuming as-is would skip the original instruction (single-byte)
    or land mid-instruction (multi-byte). on_int3 now reads the byte
    via ReadProcessMemory on a missed lookup: if it's no longer
    0xCC, rewind IP to the original-byte address and CONTINUE_ EXECUTION so the original instruction re-executes. If the byte is
    still 0xCC, propagate (compiler-emitted int3, third-party hook).
  • clear() racing an in-flight page BP fault no longer kills the
    host.
    Symmetric race: clear() restored protections and removed
    the entry while another thread was parked in on_guard_page waiting
    for the registry lock. After page::restore (under the lock),
    clear() now marks each affected page in ORPHAN_PAGES so the
    racing thread recovers via the orphan path on the next lookup. The
    marker is consumed by the first take_orphan call.
  • read_cstr_bounded clamps by readable region as well as length.
    The 4 KB hard cap limited how many bytes we'd walk but not whether
    the bytes were mapped. A malformed PE — or a normal PE with the
    export string table abutting an unmapped page — would let
    from_raw_parts(ptr, 4096) AV partway through the NUL scan. Now
    also bounds by VirtualQuery's region tail, returning None if the
    ...
Read more

v0.3.0

27 Apr 07:39

Choose a tag to compare

New: GET /logs endpoint and haunt logs CLI for tailing the agent's
own info/warn/error output. Partial-content responses surfaced by the
read CLI with a stderr warning.

Removed: OutputDebugStringA log sink (replaced by /logs).

Stability: closed several host-kill paths in the VEH (step→continue
TF leak, multi-page rearm overwrite, failed-install PAGE_GUARD
orphans, clear-vs-hit races); SW BP install now rejects pages with
pre-existing PAGE_GUARD; resume --ret no longer takes the loader
lock and accepts JIT regions; assorted ordering and lock-poisoning
fixes.

Full notes in CHANGELOG.md.

v0.2.0

26 Apr 13:45

Choose a tag to compare

v0.2.0

v0.1.1

24 Apr 12:37

Choose a tag to compare

v0.1.1: symbol lookup, stack walking, binaries workflow

- Resolve breakpoints by name (module!symbol) server-side against
  module export tables; new /symbols/resolve endpoint and `haunt
  resolve` CLI.
- Walk stacks via rbp chain, resolve frames to module+offset; new
  /halts/<id>/stack endpoint and `haunt stack` CLI.
- binaries.yml attaches cross-compiled haunt.dll, haunt-inject.exe,
  haunt.exe plus SHA256SUMS to each GitHub release on v* tag push.
- Percent-decode query values and path segments so mangled C++/Rust
  symbols round-trip correctly.
- /bp/set returns 400 for malformed names and rejects addr+name
  conflict; /symbols/resolve returns 400 vs 404 appropriately.
- README: accurate loopback-only bind note, SSH-tunnel remote-access
  recipe, module!symbol in workflow examples.
- AGENTS.md with workspace target, cdylib safety invariants.
- CHANGELOG.md added.