Skip to content

octane: free the bus and its decode table when the machine drops - #396

Merged
techomancer merged 3 commits into
techomancer:mainfrom
jamesbraid:fix-octane-devmap-leak
Nov 2, 2026
Merged

techomancer merged 3 commits into
techomancer:mainfrom
jamesbraid:fix-octane-devmap-leak

Conversation

@jamesbraid

Copy link
Copy Markdown
Collaborator

An Octane machine keeps its bus and its device-map table alive after it's dropped, and a process only has 64 tables. Build Octanes one after another and the 65th aborts with devmap: more than 64 tables alive. That happens on main too. A GUI Stop then Start would do the same, though I haven't run that.

Three things kept the bus alive, one commit each:

  • The JIT's compile workers read the page array through raw pointers and hold the bus. Jitv2 freed the pages before it joined the workers, which is a use-after-free once a CPU can actually be dropped. It now stops its workers when it drops. This commit goes first so the later ones don't expose the race.
  • The lock registry is a static that's never emptied, and the Bridge's entries held a strong reference to the Bridge. That kept HEART's memory and the bus alive for the life of the process. They hold it weakly now, as IOC3, ISP1040 and RAD1 already do.
  • The Bridge and IMPACT hold HEART's DMA view, which owns the bus, while the bus holds them. When the Octane drops, the bus now points its decode table at its own error device and lets go of the devices, so a late access gets a bus error, not freed memory. The Octane also stops forgetting a reference to its CPU, which kept the bus alive under jitv2.

Things to look at:

  • The Indy, Indigo2 and O2 still leak their tables, for other reasons. I left them alone.
  • The locks monitor command still lists a dropped machine's entries, as free.
  • Nothing changes on a hot path. The registry closures only run for the locks command.

a_dropped_octane_frees_its_decode_table builds and drops an Octane, with and without IMPACT, and checks nothing still holds the bus. It fails without the release, with and without jitv2. dropping_a_jitv2_joins_its_workers fails without the two Drop impls, but it only checks that the workers are gone, not the page-array race itself. That's covered by the lib suite passing five times in a row at eight threads under jitv2, and by the display-stop and new tests passing under AddressSanitizer. The suite also passes at one thread, with and without jitv2. Not run: macOS, Windows, a real GUI Stop then Start.

The compile workers read the page array through raw pointers and hold
the bus they compile from. Jitv2 declares pages before compile_queue,
so when the last reference went, the array was freed first and the
workers were joined only afterwards: a use-after-free. Nothing hit it
while a dropped machine's CPU leaked. The next changes let it go.

Jitv2 now stops its pool in its own Drop, and a CompileQueue dropped
alone does the same, through stop(). stop() skips the calling thread,
since a worker can hold the last reference for a moment, and it drains
the process-wide hot queue, so a dropped machine leaves no request
behind that points at freed pages.
The lock registry is a static that is never emptied, and the Bridge
registered its locks with probe closures holding a strong clone of the
Bridge. The Bridge keeps HEART and its memory alive, and the memory's
DMA view owns the physical bus, so one registry entry kept a dropped
Octane's whole memory map, and a slot in the process's 64 decode
tables, for the life of the process.

The helpers (register_mutex_fn, register_spin, register_mutex) now
downgrade the Arc they are given, and the Bridge uses them. IOC3,
ISP1040 and RAD1 already held their owners weakly. The cost is that the
locks monitor command still lists a dropped machine's entries, as free.
The Indy-family register_locks methods are left as they are.
A GUI Stop then Start on an Octane left the old machine's physical bus
behind, and a process can have only 64 decode tables: a test binary
that built Octanes one after another failed at the 65th with "devmap:
more than 64 tables alive".

Three things held the bus. The lock registry did, which the earlier
commit fixes. The Bridge and IMPACT hold HEART's DMA view, which owns
the bus, and the bus holds them; and under jitv2 the compile workers
held it, which the first commit fixes. Nothing broke the second cycle.
The machine's drop, which already stops the CPUs and devices, now calls
Physical::release_devices.

release_devices points the whole decode table at the bus's own error
device before it lets go of the devices, so an access that still
arrives gets a bus error, not a freed device. The Octane also stops
forgetting an Arc of its CPU, which kept the CPU alive under jitv2;
as_device returns the CPU's own allocation, so the pool's weak handle
needs no extra reference.
@techomancer

Copy link
Copy Markdown
Owner

we need a rebase plz

@jamesbraid
jamesbraid force-pushed the fix-octane-devmap-leak branch from 4353128 to 7fe0d54 Compare November 2, 2026 10:06
@techomancer
techomancer merged commit b0961b8 into techomancer:main Nov 2, 2026
14 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants