<feed xmlns='http://www.w3.org/2005/Atom'>
<title>ida-tui.git/idatui/pool.py, branch main</title>
<subtitle>tui for headless ida</subtitle>
<id>https://git.sl0p.foo/ida-tui.git/atom/idatui/pool.py?h=main</id>
<link rel='self' href='https://git.sl0p.foo/ida-tui.git/atom/idatui/pool.py?h=main'/>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/ida-tui.git/'/>
<updated>2026-07-25T21:49:14Z</updated>
<entry>
<title>loading: say how to read a headerless blob (processor, base)</title>
<updated>2026-07-25T21:49:14Z</updated>
<author>
<name>blasty</name>
<email>blasty@local</email>
</author>
<published>2026-07-25T21:49:14Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/ida-tui.git/commit/?id=57b09047881d4bb42b2b98e4bb0739d5bc5e5af8'/>
<id>urn:sha1:57b09047881d4bb42b2b98e4bb0739d5bc5e5af8</id>
<content type='text'>
A raw firmware dump has no format to detect, so IDA fell back to x86 at address
0. It doesn't fail — it opens, analyses, and finds nothing. An AArch64 image
loaded this way gave 0 functions; told the truth it gives 35.

    ida-tui fw.bin --processor arm --base 0x8000000

and per binary in a project, which is what a multi-image firmware actually
needs:

    {"path": "app.bin", "processor": "arm", "base": "0x8000000"}

idapro.open_database() already accepted IDA command-line switches; nothing was
passing any. Plumbed BinaryRef -&gt; WorkerPool -&gt; WorkerClient -&gt; worker argv, plus
a load_args for the single-binary path that has no project ref.

base is written the way people say it (0x8000000, int or string, any base).
IDA's -b is in PARAGRAPHS — -b1000 loads at 0x10000 — so BinaryRef.load_args
converts, and a base that isn't 16-byte aligned is refused rather than silently
landing 16x off. ida_args passes anything else through.

Two bugs found by testing the whole path rather than the happy one:

* Project.load() whitelisted path/label when normalising entries, so the load
  options were dropped the first time a project was reopened — set a processor,
  come back tomorrow, it's gone.
* Re-passing the switches to an EXISTING database makes IDA refuse the open
  (rc != 0, no functions). The .i64 already records how the image was loaded, so
  the worker skips them once a database exists. My first guard checked
  splitext(path) + ".i64" and never fired, because IDA names it "&lt;file&gt;.i64" —
  keeping the extension. It checks both spellings now.

Verified on a real AArch64 blob: fresh load 35 functions based at 0x8002440,
reopen 35 again, and the CLI rejects an unaligned or non-numeric --base.

tests: +6 project (options recorded, paragraph conversion, file round-trip,
add() takes them, an ELF passes nothing, hex-string base). 39/0 project, 195/0
scenarios, 30/0 project UI, 36/0 index, 27/0 pool.
</content>
</entry>
<entry>
<title>projects phase 4: cross-binary back, linkage-guided pre-warm, project verbs</title>
<updated>2026-07-25T21:03:10Z</updated>
<author>
<name>blasty</name>
<email>blasty@local</email>
</author>
<published>2026-07-25T21:03:10Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/ida-tui.git/commit/?id=b8d9aa4fce78f4523cffaba8b46095d2dfb66310'/>
<id>urn:sha1:b8d9aa4fce78f4523cffaba8b46095d2dfb66310</id>
<content type='text'>
Three things, all following from phase 3 making cross-binary jumps ordinary.

**A cross-binary jump was a one-way door.** Nav history is per-binary, so
arriving in another binary — a project search hit, or now following an import
into the library that implements it — landed you in an empty history with nothing
to take you back. _switch_then_goto records the binary it came FROM, and
action_back falls through to that hop once local history is spent: Esc walks back
through the function you were in, then the binary you were in. Manual Ctrl+O
switching records nothing, because that isn't navigation.

**Pre-warm follows the linkage graph, not list order.** _prewarm_provider warms
the binary providing the most of this one's imports — where a follow is most
likely to go, so its startup is paid before you ask for it. "Next in the list"
would have been arbitrary; phase 3 gave us something better to ask.

pool.prewarm() refuses rather than making room. Evicting a binary the user
visited to speculatively load one they haven't is a straight downgrade, and it
throws away that binary's caches as well; at a tight budget pre-warm just does
nothing. The cost of a worker that doesn't exist yet can only be estimated, so it
uses the largest resident one (same program, different database) — and if that
estimate proves wrong, the speculative worker is the one evicted, never a chosen
one.

**Driving a project.** pane spawn --project FILE [--open BIN]; `binaries` lists
the inventory (active / resident / indexed / where Esc returns to) and `switch
{binary,addr?}` makes another active — with an address it takes the search-hit
path, so it records a hop. state gains `binary` and `hops`, which it should have
had the moment project mode existed.

Verified on real sessions: drive binaries/switch against an echo+cat project
pane; Esc crossing back from a switch; and prewarm on echo+libc picking libc
(provider of echo's imports) and warming it after an evict.

tests: +5 pool (prewarm warms, no-ops when resident, refuses at budget, evicts
nothing when refusing, ignores unknown labels) and +4 project UI (jump records
the hop, Esc crosses back, hop consumed). Confirmed the Esc-back checks fail with
the branch removed. 195/0 scenarios, 27/0 project UI, 36/0 index, 27/0 pool,
33/0 project.

Left open: project-level persistence across sessions.
</content>
</entry>
<entry>
<title>projects: worker pool with memory-budgeted residency (phase 1b)</title>
<updated>2026-07-25T09:29:36Z</updated>
<author>
<name>blasty</name>
<email>blasty@local</email>
</author>
<published>2026-07-25T09:29:36Z</published>
<link rel='alternate' type='text/html' href='https://git.sl0p.foo/ida-tui.git/commit/?id=2d342a92f5c9995c7014b1699eaf1180aadcef26'/>
<id>urn:sha1:2d342a92f5c9995c7014b1699eaf1180aadcef26</id>
<content type='text'>
idatui/pool.py — WorkerPool keeps one live worker per project binary:
* lazy spawn on first use; staging + a scratch sweep happen first, so a database
  wedged by a previously hard-killed worker reopens instead of crash-looping.
* residency is bounded by a MEMORY BUDGET (default project.memory_pct of RAM),
  not a worker count — a count is the wrong knob when one project holds a 50KB
  helper and a 6MB crypto lib. Cost is measured per worker from
  /proc/&lt;pid&gt;/smaps_rollup (PSS, which splits shared pages so summing means
  something).
* over budget -&gt; evict least-recently-used, never the active or pinned binary,
  and give up rather than thrash when nothing is evictable. Eviction calls
  idb_save first, so returning to a binary is a DB load, not a re-analysis.
* status() feeds the switcher UI (resident/pinned/active/analysed/memory).

worker_client: fix the wedge-file bug this depends on. close() sent
__shutdown__ and then IMMEDIATELY SIGTERMed, killing the worker mid
close_database() — which is what leaves the unpacked .id0/.id1/... behind and
makes the .i64 refuse to reopen. Now it waits out a grace period (the worker
returns from serve() on __shutdown__ and closes the DB in its finally) and only
escalates if it's genuinely stuck. Also expose .pid for memory accounting.
Verified: a pilot run that used to leave echo.id0/.id1/.id2/.nam/.til now leaves
only echo.i64, and teardown is no slower (14 checks in 5s).

tests/test_pool.py: 22 checks with an injected fake client — LRU order, budget
eviction, active/pinned protection, save-before-close, thrash avoidance,
status(), teardown. Pure stdlib, no idalib.
</content>
</entry>
</feed>
