Releases: sebastian-heinz/haunt
Releases · sebastian-heinz/haunt
v0.6.0
v0.5.2
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'shaunt-{linux,macos}-x64are now
haunt-{linux,macos}-x86_64, aligning with the Rust target-triple
convention already used in the localdist/directory and removing
drift between the GitHub release page and any scripts pointing at
local builds. - macOS builds collapsed to a single
cli-macosjob. The two
per-arch matrix entries (macos-13for Intel,macos-14for Apple
Silicon) are now one job onmacos-14that cross-compiles
x86_64-apple-darwinfrom the arm64 host. Apple's clang ships both
SDKs so this is a native cross — same toolchain, same binary. This
eliminates themacos-13runner-queue dependency that left v0.5.1
stuck for ~25 min waiting on a runner allocation that never landed.
Added
haunt-macos-universal—lipo-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
*.sha256sidecars for the Linux + macOS CLI
binaries that v0.5.1 added. The Windows job's consolidated
SHA256SUMSwas 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
Added
hauntCLI prebuilt for Linux and macOS. The DLL and injector
are Windows-by-construction, but the CLI is just loopback HTTP over
std::netwith no native deps — there's no reason it should only be
available ashaunt.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 256so asha256sum -cfrom any host validates it).
The Windows job retains its consolidatedSHA256SUMSfile so
existing release-asset names are unchanged.
v0.5.0
Changed
- Trace + log ring capacity raised from 4 096 to 40 960 records. At 4 096 a short burst from a high-rate
--logBP could slide records off the front of the ring before a/eventscaller finished its setup round trip. The new bound matches the newlimit/tailceiling so a single call can drain the whole ring. MAX_LONG_POLL_TIMEOUT_MSraised from 60 000 to 300 000 ms (5 min). Operators tailing/eventsor/logsover a slow link no longer have to re-poll every minute. Both the HTTP-edge validator and the platform-sidewait_halt/events::poll/logs::pollimpls enforce the new ceiling.- CLI socket read timeout raised from 90 s to 310 s. Previous value pre-dated the long-poll cap bump;
--timeoutvalues above ~90 s would have the CLI'sTcpStreamabort the read before the agent could respond. New value covers the 300 s agent ceiling plus loopback slack.
Added
MAX_TRACE_BATCHshared constant as the single source of truth for both the events / logs ring capacity and the HTTP-edgelimit/tailvalidator. The previous duplicated4096literal inhandle_events/handle_logswould silently cap clients to the old value if the ring grew without the validator being touched.
Documentation
- README's
Concurrencyparagraph now lists/logsalongside/halts/waitand/eventsas endpoints sharing the 64-of-80 long-poll sub-cap (the classifier already routed/logsthere; the prose was stale). - CLI
USAGEforeventsandlogsnow spells out the--limitdefault (256) and max (40960 = ring size); previously users only learned the bound from a 400 response.
v0.4.0
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 a400:- Unknown query parameters are rejected by every endpoint that
iterates query (e.g./bp/set?halft_if=...— typo ofhalt_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 toContinue. - 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 byparse_query; now
route()400s withmalformed query pair (expected key=value): \foo``. Single source of truth — handlers don't have to recheck. - No more silent
clampon user-supplied counts.events/logs
?limit=0was silently raised to 1;?limit=99999was silently
capped to 4096;events?tail=0was silently raised to 1; same
fortailupper 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=...
overMAX_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 thanrawwas silently treated ashex; now
format=hax400s rather than yielding hex output.
- Unknown query parameters are rejected by every endpoint that
Fixed
resume --retnow logs SW-BP install failure. The one-shot SW
BP planted at[xSP]was installed vialet _ = super::set(...)
— anyConflict/Unwritable/etc. error was silently dropped and
the user got200 resumedwhile the thread ran free past the
function with no halt. Nowwarn!s with the failure reason, visible
viahaunt logs.reject_page_covering_sw_bpoverflow. The neighbouring
page_addr.checked_add(page_size)correctly rejected wrap, but
end.saturating_add(ps - 1) & !(ps - 1)could wrapend_pagedown
pastendand silently miss a SW BP on the last covered page. Now
useschecked_addsymmetrically withpage::install.dsl::renderno longerunwrapswrite!to aString. The
unwrap was infallible in practice (fmt::Write for Stringnever
errors) but violated the no-unwrappolicy; a future refactor that
swapped the sink for something fallible would have turned a render
bug into a host-process abort. Now useslet _ = 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, nounwrap, no
unwrap_oron user input, no silent defaults — non-negotiable.
Added
events --tail Nreturns the most recentNmatching records
in chronological order, regardless of--since. Disables long-poll
(a snapshot, not a wait). Solves the ring-overflow foot-shape where
--since=0slid off the front of the deque while the caller was
setting up. Server:?tail=Nquery param; long-poll suppressed when
set.events --bp-id Nserver-side filter — only records from BP
Ncome back, useful when several BPs fire at high rate and the
client only cares about one. Server:?bp_id=Nquery param.- CLI-side address annotation in
haunt events/haunt logs.
Hex sequences in recordmsgfields that fall inside a loaded
module are annotated inline as0x... (module+0xoffset)against
/modules. Default-on;--no-annotatefor stable output in
scripts. Done CLI-side rather than VEH-side because module
enumeration takes the loader lock — calling it from the VEH (where
--logrecords are emitted) is a deadlock vector we deliberately
avoid for the same reasonresume --retwas moved off
modules::listtoVirtualQuery.
Changed
--ifsplit into--log-ifand--halt-if. The original
singlecondgate 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,--ifis removed; HTTP?cond=is
removed in favour of?log_if=and?halt_if=.bp list/
bp infooutput now haslog_if=...andhalt_if=...fields in
place ofcond=....entry.hitscontinues to count every fire
regardless of either gate.
Added
GET /logsendpoint andhaunt logsCLI for tailing the agent's
owninfo!/warn!/error!output. Mirrors/events: bounded
ring (4096), monotonic id, long-poll up to 60 s, same?since=&limit= &timeout=query shape. Replaces the previousOutputDebugStringA
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
OutputDebugStringAlog sink inhaunt-windows(DebugStringSink).
Usehaunt logsto drain the agent's output instead.
Fixed
- Step → continue from a HW BP halt no longer kills the host.
apply_resume_mode(Step)setTRAP_FLAG; nothing cleared it on the
subsequentContinue(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_stepfound no rearm / no
STEP::Step/ no DR slot fired, fell through toEXCEPTION_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(withapply_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_PAGEwas a singleCell<Option<PageRearm>>. A misaligned
load crossing a page boundary,rep movsover 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 aCell<Vec<PageRearm>>(capped at 64 entries; uses
Cell::take/Cell::setrather thanRefCellto stay panic-free). - Failed page-BP installs no longer leak
PAGE_GUARDorphans.
Two paths produced orphan-guarded pages with no registry entry,
killing the host on the next access: (1)query_protectfailure
mid-loop bailed via?with no rollback, leaking guards on every
already-protected page; (2)set_protectfailure rolled back, but
if a rollbackVirtualProtectitself failed the page stayed
guarded. Both paths now go through one rollback that records any
unreversible page inORPHAN_PAGES. The VEH consults the set on a
guard fault that doesn't match any registered BP and returns
EXCEPTION_CONTINUE_EXECUTIONinstead 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_GUARDset.
write_byte'sVirtualProtect(PAGE_EXECUTE_READWRITE)is page-
granular and silently stripsPAGE_GUARDfor the duration of the
byte write — defeating the OS stack-growth guard, AV sentinels, JIT
runtime traps, and foreign debuggers usingPAGE_GUARD. Now
VirtualQuery-checked at install time and rejected withConflict
/409. Symmetric withreject_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 andon_int3acquiring 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_int3now reads the byte
viaReadProcessMemoryon a missed lookup: if it's no longer
0xCC, rewind IP to the original-byte address andCONTINUE_ EXECUTIONso the original instruction re-executes. If the byte is
still0xCC, 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 inon_guard_pagewaiting
for the registry lock. Afterpage::restore(under the lock),
clear()now marks each affected page inORPHAN_PAGESso the
racing thread recovers via the orphan path on the next lookup. The
marker is consumed by the firsttake_orphancall.read_cstr_boundedclamps 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 byVirtualQuery's region tail, returningNoneif the
...
v0.3.0
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
v0.2.0
v0.1.1
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.