JIT native-memory ownership¶
Investigation¶
Before this change, compile_bodies in gossamer-codegen-cranelift/src/jit.rs
created a JITModule, finalized it, copied each callable entry pointer into a
JitFn, and stored the entire module in JitArtifact. JitArtifact::drop
called JITModule::free_memory. The interpreter retained the artifact in
JitState and in a weak per-thread cache. Dispatch maps retained Arc<JitFn>
metadata, but the VM's Rc<JitArtifact> was the actual allocation owner.
Only finalized function pointers leave the module. Gossamer does not retain a
Cranelift function ID, data ID, declaration reference, compilation context, or
module lookup handle for runtime use. JitFn additionally retains compact
Gossamer metadata: the source name, parameter kinds, result kind, and the
fresh-result aliasing flag.
Cranelift 0.134.2 routes all generated allocations through the public
JITMemoryProvider interface. The allocation kinds are executable, read-only,
and writable. Executable blobs include machine code, inline constants, jump
tables, and AArch64 call veneers. Read-only allocations contain finalized
constant data. Writable allocations contain mutable native data. Gossamer
builds the JIT with position-independent code disabled, as required by
JITModule, so Cranelift rejects GOT and PLT relocation forms. Direct and
absolute relocations, including references to imported runtime functions and
other compiled functions or data, are applied before provider finalization.
All relocation targets therefore resolve either into provider-owned mappings
or to process-lifetime symbols in the Gossamer executable.
The public SystemMemoryProvider uses anonymous memmap2 mappings on Linux,
Windows, and macOS. During finalization it changes read-only pages to read-only
and executable pages from writable to read-execute. It also clears the
instruction cache, flushes the instruction pipeline, and applies AArch64 BTI
where supported. Writable data remains non-executable. Sharing this provider
preserves Cranelift's supported W^X and cache-coherence behavior instead of
duplicating platform memory code.
Gossamer sets Cranelift's unwind_info flag to false. The optional
cranelift-jit/wasmtime-unwinder feature is not enabled. Consequently,
JITModule owns no Windows function table, DWARF registration, compact-unwind
registration, or Wasmtime exception table needed by Gossamer's emitted code.
Language panic operations call process-lifetime Gossamer runtime functions;
the VM keeps panic-capable entries on bytecode. Cranelift traps are terminal
unreachable paths after those runtime calls rather than a module-owned trap
registry. This is the same arrangement on Linux, Windows, and macOS.
Gossamer already reclaimed JIT code at safe VM teardown by calling
JITModule::free_memory. It does not need individual-function reclamation.
Worker VMs complete before teardown, artifacts are thread-confined, weak cache
entries do not retain artifacts, and dispatch entries are cleared with their
owning VM state. The implementation therefore preserves whole-artifact
reclamation and does not add fine-grained unloading.
Safety decision¶
The ownership gate passes with Cranelift 0.134.2's public API. A custom adapter
can share the complete backing provider with a lightweight artifact. Ordinary
JITModule destruction drops only its declarations, ISA, symbol maps,
compiled-blob relocation vectors, and its adapter reference. It does not call
JITMemoryProvider::free_memory. The artifact's reference keeps every native
mapping alive. The final reference explicitly invokes the public provider's
free_memory operation.
The runtime ownership graph is:
NativeCodeHeap
|-- temporary GossamerJitMemoryProvider adapter, owned by JITModule
`-- JitArtifact reference, owned by the VM during native execution
The lifecycle is building, finalized, detached, then retired. Allocation is accepted only while building. Provider finalization establishes page permissions and cache coherence before the state becomes finalized. Entry pointers are copied out, the module is explicitly dropped, and only then is the heap marked detached and returned. Artifact destruction changes the state to retired before releasing mappings. There is no lock, reference-count operation, or lifecycle check on native dispatch.
On compilation or finalization failure, local references and the module adapter
are dropped. The last NativeCodeHeap reference then frees all partial
allocations. A successful artifact owns its mappings until VM teardown. Native
entry pointers never escape without the owning artifact, and Gossamer's
thread-confined VM model prevents artifact destruction racing an active call.
Platform behavior¶
Linux, Windows, and macOS all use Cranelift's public SystemMemoryProvider.
No platform-specific allocation implementation is added. There are no helper
processes, compiler or linker invocations, object files, generated shared
libraries, or runtime toolchain dependencies. The JIT remains entirely inside
the Gossamer executable and normal operating-system mappings.
Windows and macOS execute the same post-module-drop integration tests in the
existing native CI matrix. Any future enabling of Cranelift unwind metadata or
the wasmtime-unwinder feature invalidates this analysis and must add an
explicit registration owner before detachment remains allowed.
Reclamation boundary¶
Whole-artifact executable-memory reclamation remains supported at safe VM teardown. Early reclamation of individual functions is not supported. It would require proof that no native stack, function value, dispatch slot, callback, or other compiled artifact can reference the function, which the current VM does not track at that granularity.
Linux measurements¶
The release executable was measured twice with /usr/bin/time, using the
checked-in benchmark inputs. The baseline is the previously recorded
gos-jit result in each suite. It predates both the Cranelift 0.134.2 upgrade
and detachment, so these numbers establish the current result but do not by
themselves attribute every byte to detachment.
| benchmark | input | recorded peak KB | detached median peak KB | change KB | recorded wall s | detached median wall s |
|---|---|---|---|---|---|---|
| fasta | 25,000,000 | 22,040 | 20,498 | -1,542 | 3.27 | 3.24 |
| n-body | 50,000,000 | 23,400 | 22,012 | -1,388 | 2.07 | 2.09 |
| fannkuch-redux | 10 | 21,520 | 20,380 | -1,140 | 0.36 | 0.37 |
| spectral-norm | 5,500 | 21,712 | 20,294 | -1,418 | 2.76 | 2.78 |
| mnist-slp | 2,000 | 23,120 | 22,014 | -1,106 | 0.34 | 0.34 |
| edit-distance | 10,000 | 22,132 | 20,576 | -1,556 | 0.75 | 0.75 |
| json-serde | 50,000 | 117,520 | 115,850 | -1,670 | 0.14 | 0.15 |
For n-body, an instrumented run reported 20,602,880 bytes immediately before
compilation, 24,244,224 bytes immediately after module drop, and 23,285,760
bytes at execution completion. The process high-water mark was 23,994,368
bytes. The n-body artifact contained 7,027 generated native bytes. Compilation
itself still determines the high-water mark; detachment reduces the live set
retained for native execution. Native byte counts are reported separately by
GOS_JIT_TRACE and the existing gos bench metrics.
Timings are coarse wall-clock samples, but show no systematic regression. The native call path is unchanged and does not consult the heap owner.
Validation¶
Linux debug and release tests explicitly call finalized entries after module drop. They cover integer and floating-point ABIs, recursion, native-to-native calls, runtime calls, writable statics, struct results, divide-by-zero panic behavior, artifact reuse, and repeated create/call/drop cycles. The full Cranelift crate suite and the 19 focused interpreter JIT tests pass in both profiles.
The existing native CI matrix runs the same executable JIT tests on Linux x86-64, Linux AArch64, Windows, and macOS. The new integration test is not in the platform-invariant skip list, so CI executes it on Windows and macOS rather than merely compiling it.
A local x86_64-pc-windows-gnu check passes. The local macOS cross-check cannot
build C dependencies because no Apple C cross-toolchain is installed; the host
cc rejects Apple -arch and deployment-target flags. Native macOS execution
therefore remains a CI validation item.
The full workspace test command is currently blocked by unrelated compile
errors in gossamer-interp tests at vm/call_dispatch.rs:603 and :605.
Those errors concern string comparison in concurrent Phase 6 work and were not
changed as part of this phase.