Repository navigation
Hydra Engine 12.3-hydra.2
Released 2026-10-26. Based on upstream Ghidra master (12.3 DEV) as of 2026-10-26,
commit 057a646233edfb21768a42a0aabc1d1a7709da19: the same upstream commit as 12.3-hydra.1
(upstream had no newer commit when this release was cut).
What's new for integrators
Several agents can now change things at the same time, each in the way that fits its work:
- A working copy of a project (
fork_project) while every other client keeps reading the
master: a block clone on ReFS (about 0.2 s and 3-5 MB for a 1.5 GB project), a full copy
elsewhere (8.5 s for the same project to NTFS), with the source
and base revision recorded next to the fork. - A private overlay of a program others read (
open_programwithoverlay,
apply_overlay): renames, labels, comments, signatures and data types only that client sees,
never saved unless promoted into a working copy (promote_overlay). - Several writers: several clients write one project (different programs in parallel), or
one program in shared mode ("share": true), with every change journalled with its author,
expectagainst lost updates, an atomiceditcommand anddiscard_edits. - Revisions and
refresh: answers say when another process saved a project a read-only
view serves (stale);refreshreopens it for all its clients at once. - Who holds it, and what to do:
busy,conflictandnot_ownername the holder
(holderInfo) and suggest the action (retry,fork_projectwith a ready request, an
overlay,share,reread,open_read_only). - Imports in parallel (
maxImports), with a visible, cancellable queue instatus.imports. - Per-request
client/harnesslabels, so one MCP server serving several agents can tell
them apart in holders,status, the import queue andcancel. - A versioned integration contract:
helloandstatuscarryprotocol,minClient,
hydraVersion,ghidraVersionandinstallId; the client ships as its own release asset,
hydra-client-12.3-hydra.2.zip, and refuses an engine it cannot drive. The export contract
of long scripts (artifact path, progress, cancel, partial files) is documented and tested.
Compatibility
- Protocol 2, unchanged. Everything above is new commands and new fields; nothing was
renamed or removed, except that thebusydetails of a program lock carry the program's key
inprogram(its project path moved topath). The engine answersminClient: "1.0.0". - Client 1.0.0 (
ghidra_session.py,CLIENT_VERSION) is the first versioned client;
pin it together with the engine. The unversioned client of 12.3-hydra.1 still speaks protocol
2 but does not check the engine. - Projects: a project saved by this build cannot be opened by Ghidra 12.2 or 12.1.4
(upstreammasterwrites data type databases at version 3). Projects analysed with 12.1.4
open read-only without being written, upgraded in memory and then served from the upgrade
cache;fork_projectmakes a writable copy that leaves the original as it was. The
legacy-12.1.4branch remains for projects that must stay on 12.1.4. - Platforms: built and tested on Windows 11 x64 only (locally and in the release
workflow). Linux and macOS are not released or tested routinely.
Downloads
hydra-engine-12.3-hydra.2-win_x86_64.zip: the headless install for Windows x64 (gradle zipHeadless); the release workflow ran the three test suites on it after unzipping it.hydra-client-12.3-hydra.2.zip:ghidra_session.py(standard library only) stamped with
this release,LICENSEand a shortREADME.txt.SHA256SUMSfor both.
You need a JDK 25 and Python 3.10 or later; the install has no JDK.
Known limits
- The data type database version 3 limit above, and a development snapshot as the base
(upstreammasteris Ghidra's next version under work, not a Ghidra release). - Overlays change only through their five operations (rename, label, comment, signature, data
type) and show the program as stored when they opened, not other writers' unsaved changes. - A fork is a block clone only on ReFS (Windows 11 24H2 and later); on NTFS it is a full copy,
whichautomode refuses aboveforkCopyLimitMb(default 2048). - The author journal lives in the daemon's memory while the program is open and keeps the last
200,000 entries;discard_editsreverts renames, labels and comments only. - Headless only: no window, help, javadoc or Python type stubs.
Added
-
Holder information in
busy,conflictandnot_owner(HeadlessSession). The error
details name who holds the project, program or lock:holderInfo(client,name,
harness,label,command,heldMs; for another process, the user, host and time from
Ghidra's project lock file and whether that process is alive), the project or program identity
(project,gpr,programkey,path,sha256), andsuggest:retry,
fork_project(with a readysuggestionrequest) where a working copy is the answer,
share(join a shared program),reread(a value changed since it was read), or
open_read_only.holderstays a string as before; everything else is new fields. -
fork_project(HeadlessSession): copies a project into a working copy for the caller
while the source and its read-only views stay open and in use. A block clone on ReFS (Windows
11 24H2 and later), else a full copy;modeauto(clone, or a full copy up to
forkCopyLimitMb, default 2048),copy-on-write(clone or refuse) orfull. The answer has
the new project id, the paths, the source's identity and base revision,method,cloned
(measured),bytes,filesand times. The fork records its source, base revision and lineage
in<name>.fork.jsonnext to its.gpr, shown asforkbyproject. A source another
client or process writes is refused, naming it. New launcher settingforkCopyLimitMb
(GHIDRA_SESSION_FORK_COPY_LIMIT_MB), daemon option-forkCopyLimitMb. -
Revisions and
refresh(HeadlessSession): every project and program has arevision
(a fingerprint of the saved files), shown byopen_project,project,projects,
open_program,info,saveandstatuswithdiskRevision,staleandgeneration.
Answers carrystale(a read-only view whose project another process saved) and, once,
refreshed(another client refreshed it). New commandrefresh(aliasreopen) reopens a
read-only view for all its clients at once, under the same keys. The Python client gains
call_rawandlast_response. -
Integration contract (
Ghidra/Features/HeadlessSession,support/ghidra_session.py):
hello,statusand the endpoint file now carryprotocol,minClient,hydraVersion,
ghidraVersion,installIdandstartedAt; the Python client has aCLIENT_VERSION
(1.0.0) and refuses, withIncompatibleError(exit code 4 on the command line), an engine of
another protocol or one whoseminClientis newer than itself;statusandstopstill work
on such an engine.ghidra_session.py versionprints the client's version. README: "Pinning
and upgrading", and a compatibility statement for projects analysed with Ghidra 12.1.4. -
Client release artifact: the release workflow also publishes
hydra-client-<version>.zip
(the standard-library-onlyghidra_session.pystamped with the release,LICENSE, a short
README.txt), listed inSHA256SUMS..github/scripts/package-client.shbuilds it and
checks the packaged file; CI runs it as a dry run. -
Status shows requests: each client in
statuslists its unanswered requests (id,cmd,
artifact,ms,message,progress,maximum) and itsclientVersion. -
Script exports:
scripttakes an optionalartifact(a path the script writes), echoed in
statusand in the result;examples/scripts/ExportJsonl.javais a sample export with
progress and an inspectable partial file; README "Scripts" states the export contract
(output outside the project, not capped bymaxChars, partial files after cancel and shutdown). -
Concurrency documentation: README "Who can do what at the same time": readers, writers,
projects, imports, and whatconflict,busyandnot_ownermean. -
Tests:
examples/contract_tests.py(identity fields, client compatibility, a 600,000-line
export with progress, cancel and shutdown), run by CI and the release workflow. -
The import queue is visible and cancellable. Imports that cannot run yet (one at a time
when this was added; up tomaxImportssince "Imports run in parallel" below) queue
first-come, first-served without holding a scheduler slot.status.importsshows the
holder (client label, project, file, running time), the queue with each entry's position and an
estimated wait (from the durations of earlier imports), and the history. A queuedimport
sendsprogressevents "waiting behind ..." and its answer reportsqueue(waitedMs,
positionAtEntry,behind). Cancelling a queued import removes it without touching the
running one; cancelling the running one starts the next. A numericwaitnow limits the queue
wait; without one a queued import waits until it runs or is cancelled. -
Per-request
clientandharnesslabels. Any request may carry them; they replace the
connection's name for that request inbusy/conflictholders (holderandholderInfo),status(requests, scheduler
lanes, owner of a writable program,youAre), the import queue and the log, and they scope
cancel(allreaches only that label's requests;targetrefuses another label's request).
Ownership is unchanged (still per connection). One MCP server serving several agents can now
tell which agent holds a program or an import.ghidra_session.py: labels on anycall, and
labelled(client=, harness=)views withcancel_all(). -
daemon_tests.py:import_queue,import_isolation,caller_labels;--raw-mb. -
Shared writing (HeadlessSession):
open_programwith"writable": true, "share": true
lets several clients write one program at once. Each change is its own transaction,
committed one at a time under the program's exclusive lock and visible to all; last writer
wins, andexpecton renames, labels and comments (also aseditoperations) turns a lost update into
conflict(reason: "changed") naming the last editor. Every change to a writable program
is journalled with its author (journal;journalSeq,unsavedEdits,savedSeq); new
commandsedit(operations, all or none) anddiscard_edits(revert one author's unsaved
renames, labels, comments). A writer that dies loses only its uncommitted change; when it was
the last writer, the unsaved work is kept (program and project stay open) until a client
joins and saves or discards it. Owner and shared mode do not mix on one program. Tested with
4 clients x 2 threads, 3,200 edits and 64 saves: none lost, journal complete, reopen clean. -
Several writers in one project (HeadlessSession):
open_projectwritable while other
clients use the read-only view now reopens it writable in place (same id and keys, their
programs reopened, overlays rebuilt,refreshedonce) instead ofconflict; refused only
when another process holds the project lock. A second writable open joins; writers of
different programs of one project work in parallel (tested with concurrent renames,
comments, analysis and saves).not_owneraddsalternatives: ["overlay", "fork_project"]
and anoverlaySuggestion. -
Overlays (HeadlessSession): a client's private annotation view of a shared program.
open_programwithoverlayopens one: its own program object over the stored file
(changeable, never saved), measured at 116 ms and +54 MB heap for a 243,855-function program.
apply_overlayappliesrename,label,comment,signatureanddata_typeoperations
atomically and logs them;info,functions,decompile,script(contained),renameand
commenttakeoverlay;drop_overlay,overlays;promote_overlayreplays the log into a
writable copy (a project the client opened writable, or a new fork) and saves it. Each overlay
showsoverlayVersion, the program's SHA-256 and revisions, in its answers,infoand
status.overlays. A refresh rebuilds overlays from their logs; a client's disconnect drops
its overlays.
Changed
- Imports run in parallel (HeadlessSession). Up to
maxImportsimports (new launcher
setting and daemon option-maxImports; default one per four CPUs, 1 to 4) run at once, into
different projects or into one; more queue as before. Imports no longer set the process-wide
active project: nothing on Ghidra's load path reads it (the README's "Parallel imports" lists
what the load path shares and why it is safe): four 12-25 MB PEs imported at once took 64 s
against 165 s one after the other, with identical programs. The gate covers the load and the
save;analyze: trueanalyses afterwards in the import's long slot, as before. The same file sent twice at
once into one project is imported once: the second waits and answersexisting, with
sameFileImport.status.importsgainsmaxImports,runningandhistory.peakRunning
(holderis the oldest running import); an import'squeuegainsrunningAtStart. Opening an
upgrade cache copy no longer goes through the import gate. daemon_tests.py:parallel_imports,import_kill,import_readers,import_speed
(--big,--big-analyze);import_queueruns withmaxImports1.- The project files the upgrade cache keys and copies leave out Ghidra's transient files in a
.rep(tmp<n>.pspre-save copies of programs open for update,tmp<n>.tmp), so that a
project another process has open for update keeps its identity until it is saved. Cache
entries made before this are rebuilt once. - The
busydetails of a program lock carryprogramas the program's key
(p1:/path), like every other error, instead of its project path (now inpath). - A
scriptresult'schangednow says whether the script changed the program (always false
for a contained script, whose changes are rolled back); the newunsavedsays whether the
program has unsaved changes, which is whatchangedused to report.
Fixed
- A writer's read-only script was journalled as a change (HeadlessSession). A script run by
an owner or shared writer of a program with unsaved changes (from anyone) answered
changed: trueand added ascriptentry to the author journal, although it changed
nothing; on a shared program every query script did so. The session now compares the
program's modification number before and after the script. Found bydaemon_tests.py edit. - A program killed while being written into a project was listed but could not be opened
(Ghidra,Framework/FileSystemLocalDatabaseItem.create). Ghidra wrote the new item's
property file, with its file type, before streaming the database in, so a process killed
mid-write (an import's save, or a Save As) left an item that every later open listed as a
program and failed to open ("Unrecognized file format"). The file type is now written once
version 1 of the database is complete; an item killed earlier is removed by Ghidra's existing
repair the next time its folder is listed. Found bydaemon_tests.py import_write_kill. killreturned before the daemon's process was gone (ghidra_session.py). It waited for
the instance lock only; a daemon killed during a large write on ReFS kept its project lock
for seconds more (closing a file with many dirty pages waits for them), so a daemon started
right after gotconflict("locked by another process",live: true).killnow also waits
for the process to exit (within its 30 s). Found bydaemon_tests.py import_write_kill.- A daemon started after a crash could take minutes to become ready (HeadlessSession).
Ghidra'sDBInitializerdeletes the buffer cache files dead processes left in the user temp
folder before start-up goes on; withtmpdiron ReFS, deleting the large cache file of a JVM
killed while it held a big program waits until ReFS has written its dirty pages back
(measured: 43 s, 61 s and once 222 s, beyond the launcher's 180 s wait). On a class cache hit
the session now does this cleanup itself on a background thread, and on Windows renames each
file first (which fails while any live process has it open), truncates it to 0 bytes (which
drops its unwritten pages) and deletes it: the next daemon was ready in 0.35 s. Found by
daemon_tests.py import_write_kill.
Tests
daemon_tests.py:edit(theeditcommand, all or none,expectnaming the author;
discard_editsdeleting newly created labels),overlay_owned(an overlay of a program
another client owns writable, with the owner saving while it is open),refresh_force,
legacy_overlay(overlays of a 12.1.4-format program, during and after its upgrade cache
build),orphan_shutdown(orphaned shared-write work discarded and logged at shutdown),
import_write_kill(the daemon killed during an import's final write: the program absent or
complete, never half).parallel_importscancels its import during the load: on a fast CI runner its fixed 1 s
delay let the cancel arrive during the final write, which is not cancellable (README,
"Parallel imports", now says so).
Download and testing
Built by the release workflow from 3463ee2fa39e1ad77a0e13bfdaec6b46837323f5, then unzipped and tested there.
| File | Size | Built and tested on | Result |
|---|---|---|---|
hydra-engine-12.3-hydra.2-win_x86_64.zip |
361 MiB | windows-latest (Windows-2025Server, openjdk version "25.0.4.1" 2026-08-18 LTS) | built; session_smoke.py, daemon_tests.py and contract_tests.py passed on the unzipped install |
SHA-256 (also in SHA256SUMS):
f5cc0d721d401660556e28af6c104ee75a23fdc62803a419789d799a00ee6218 hydra-engine-12.3-hydra.2-win_x86_64.zip
f26e9ebc3d242d1f8ad4872acd2c5219addfc7a615c4e4334d7189c81792c773 hydra-client-12.3-hydra.2.zip