CratonVM
Changelog
Changelog
All notable changes to CratonVM will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
Unreleased
2026-08-06 The ThreadPoolExecutor.execute receiver-shape special case is gone
Nine dispatch sites across four files decided whether to run
ThreadPoolExecutor.execute's real bytecode by reading the receiver's
workers field — eight receiver-shape probes plus the one receiver-blind
force_native_over_real_jdk_bytecode arm they existed to override. All nine
and the probe helper are deleted.
What replaced them is class-scoped and lives at registration:
native_es_execute is tagged NativeKind::SyntheticStub and
java/util/concurrent/ThreadPoolExecutor joins the real-protected-stub
allow-list, so the one centralised arbitration yields it to the real
execute() body for every receiver, on both the warm and the cold dispatch
path. That became correct once the entry above removed CratonVM's ability to
mint a fabricated executor at all. The native is not deleted — strict mode
declines to admit it, and the --features synthetic-jdk build still runs it,
which is the only build where the real execute() bytecode is absent.
No behaviour change on a real JDK image: probes/L10ThreadPoolInitProbe,
JdkOnlyCensusLoadProbe and the three-arm strict-corpus gate are unchanged in
both modes, and the registry census moves by exactly the two retagged
registrations (bridge 10,434 → 10,432, synthetic-stub 755 → 757, total
unchanged). Stub ratchet re-frozen 553 → 555 — no new fake; two registrations
that were mis-tagged Bridge are now counted where they belonged.
See jdk-only-wave2-threadpoolexecutor-execute-receiver-shape-RETIRED-20260806.md.
2026-08-06 Class::is_synthetic_stub is deleted; ClassOrigin is the only answer
The bool answered two different questions — is this a compatibility
substitution? (the census and --jdk-only policy question) and does this
class have no class file, so dispatch must look for a native under its own
exact name? Splitting them is what let java/lang/reflect/Proxy$Instance be
reclassified honestly: it is ClassOrigin::VmInternal, a generation artefact,
not a stand-in for bytes that were never found — while keeping the three
dispatch sites that genuinely need it, which now ask
Class::dispatch_lacks_class_file.
A fabricated $$Lambda / $ProxyN / Generated*Accessor* is likewise
reported as what generated it, the same answer the define-from-bytes path
already gave those names.
--dump-class-origins on a dynamic-proxy workload: 420 rows before and after,
compatibility-stub 14 → 13, vm-internal 1 → 2 — exactly one class moved,
and under --jdk-only that probe now fabricates none at all.
See jdk-only-wave2-vm-internal-classes-mislabelled-RETIRED-20260806.md.
2026-08-06 Executors.new* returns real JDK executors in real-JDK mode
java.util.concurrent.Executors' pool factories are no longer intercepted when
CratonVM runs against a real JDK image: the real Executors bytecode constructs
every executor, so a factory-made pool is built by the genuine
ThreadPoolExecutor.<init> rather than by a native that allocated the object and
then tried to reproduce the constructor.
User-visible fix. Executors.newSingleThreadExecutor() returned a bare
ThreadPoolExecutor where the JDK returns
Executors$AutoShutdownDelegatedExecutorService wrapping one. Every
instanceof ThreadPoolExecutor on the result flipped, and the pool the JDK
guarantees is unconfigurable accepted setCorePoolSize. It now matches HotSpot.
Also removed: two fallbacks in the old construction path that wrote a two-slot placeholder shape onto a real-layout object and returned it as if construction had succeeded. Nothing observed them firing, but while they existed an executor could be half-built, which is the receiver shape nine dispatch sites in the interpreter exist to detect.
probes/L10ThreadPoolInitProbe (new) is byte-identical to HotSpot 25 under both
--real-jdk and --jdk-only. A diagnostic added with it, CRATONVM_DBG_TPE_SHAPE=1, reported every
ThreadPoolExecutor.execute receiver-shape decision; it was removed the same
day together with the predicate, when L11 item 7 deleted all nine dispatch
sites (see below). The
--features synthetic-jdk build is unaffected — it has no real Executors
bytecode to fall back to and keeps its own factories.
See L10-blocker-threadpool-init-DONE-20260806.md.
2026-08-05 CPU benchmark table re-measured in a quiet window; Sieve at parity
All seven CratonBench rows re-taken in one interleaved series on dev
@ ded183df8 against JDK 25.0.3, in a window opened only once the load fell
below 2.5 and nothing else was pinned to the measuring core. CratonVM's
run-to-run spread is under 1% on five of the seven rows.
| ratio | was 2026-07 | |
|---|---|---|
| Arithmetic | 1.95x | 2.44x |
| Fibonacci(44) | 5.89x | 2.79x |
| Sieve | 0.99x | 2.28x |
| Matrix | 0.99x | 2.93x |
| HashMap | 2.07x | 1.75x |
| String/Regex | 5.37x | 7.7x |
| Binary Trees | 9.46x | 8.34x |
Two rows are now at parity with HotSpot C2. Sieve's 6.50x of 2026-08-04 was a live regression and is fixed; see the entry below.
Sieve's HotSpot arm is bimodal — ~2,369 ms or ~2,739 ms with nothing between, so a 9-sample median reports whichever mode won, and two consecutive series on an unchanged binary read 2,386 ms and 2,734 ms. That row's figure is the median of 18 pooled samples; the cleanest single series would have claimed 0.87x, i.e. CratonVM 14% faster than HotSpot, which the data does not support. CratonVM's own samples on that phase are unimodal.
2026-08-04 The optimizing tier stops taking methods the single-pass backend does better
cov-02 taught IrBuilder::build to lower bastore. The side effect was that
CratonBench.sieve([ZI)I stopped falling through to the single-pass backend —
which vectorises its boolean[] loops — and started getting a scalar IR
body. 2,462 ms became 15,823 ms, on a phase where CratonVM had been faster
than HotSpot C2, with an unchanged checksum and no failing test.
The general problem: the optimizing tier installs its body whenever it can, and nothing checks that the body is faster than the one the single-pass backend would have installed.
Added
jit/src/x64/single_pass_only.rs— the enumeration of what the single-pass backend can do that the optimizing tier cannot: seven classes, each consumed by a single-pass emitter at a loop header, each without a counterpart inir_optimize/ir_lower(three bulk byte-array lowerings, four vectorising ones). The admission chain consults it and its verdict names which lowering it protected. The IR tier has no vectoriser at all, socov-02hitting one of these was not bad luck — four more of the same shape were waiting.CRATONVM_JIT='-c1-vector-veto'— hand those methods back to the IR tier. Default on; the switch exists so the veto is bisectable and so its blast radius can be measured on one binary rather than argued across two.
Changed
x64/driver.rs's three inlined bulk-byte detector loops are now one call toescape_analysis::detect_bulk_byte_loops, shared with the veto, so the emission path and the admission chain cannot disagree about what the backend would emit.- Corrected two stale comments in
ir_optimize.rs:unrollandlicmare default-ON, not "Default-OFF while it soaks". They are why the single-pass unroller and hoists are not on the veto list, so the stale claim was load-bearing in the wrong direction.
Notes
- Blast radius, measured with the off-switch on one binary across all ten benchmark phases: exactly one IR body, the one that was 6.4x slower.
- Loop unswitching was in the first draft of the list and is not in it: its emitter's own contract says the sequence is additive and "removing the emission yields identical final state". Vetoing on it would have cost IR bodies for every loop with an invariant branch to protect nothing.
- Still open: the enumeration catches an advantage somebody wrote down, not one
nobody did. Closing that needs a backend-parity harness that compiles a
corpus both ways and compares emitted bytes — see
perf-01-sieve-ir-body-slower-than-c1-FIXED-20260804.md.
2026-08-03 Perf gate: it records its own C2 reach, and it can compile its benchmark again
Two changes to regression-suite/perf/, from docs/known-issues/c2/'s MEAS-02.
The gate could not compile bench/CratonBench.java. Its own
export LC_ALL=C — correct, for the awk distribution arithmetic — makes a
javac that derives its source encoding from the platform charset (17 on the
bench host) default to US-ASCII, and the benchmark's header comment has
em-dashes. Every run died at setup with 30 unmappable character errors before
measuring anything. The bench host's ambient locale is C.UTF-8, so the same
command run by hand succeeded and the failure appeared only inside the gate.
Pinned with -encoding UTF-8.
Every run now records the optimizing tier's per-phase reach. Across all seven CratonBench phases the C2/IR tier is asked 8 times, admits 3 and produces 3 bodies — so the gate measures the single-pass backend, and a CratonBench delta is not evidence about C2 in either direction. That fact now travels with the numbers instead of having to be rediscovered.
Added
ir_requests/ir_admitted/ir_bodiesinsamples.tsvandsummary.tsv; oneir_reach_<phase>line per phase inmanifest.tsv, plusir_reach_total,ir_reach_recordedandir_reach_scrape_broken; a reach summary on the console at the end of every run and of every--calibrate.regression-suite/perf/c2-reach.sh— any workload's C2 reach in one run, with two consistency checks that refuse rather than report a zero when the scrape is reading a log that no longer says what it expects.bench/CratonBenchC2.java— a candidate workload with a framework-shaped node mix, reaching the tier 36/17/11 across three phases. Deliberately not a gate phase and with no baseline; seemeas-02-bench-suite-c2-reach-RETIRED-20260803.md.
Changed
- Results schema 1 → 2; the default results directory is now
regression-suite/perf/results/v2/. Every existing column kept its name and every consumer resolves columns by name, so a v1 reader reads a v2 directory correctly. compare.pyreportsC2-tier compiles,C2 admittedandC2 bodiesseparately, and names the phases whose delta is not evidence about the optimizing tier.compiles_c2alone never was that number: it counts compiles whose requested tier was C2, including every one the optimizing pipeline declined and handed back to the single-pass backend, and including OSR compiles.- The gate asks for its VM summaries with the grouped
CRATONVM_DBG=spelling, so a run no longer opens stderr with a legacy-variable deprecation line.
2026-07-31 JDK-only mode (--jdk-only) — provenance instrumentation, wave 1
A new runtime compatibility policy: --jdk-only declares that real JDK class
bytes are authoritative, so no non-array class is fabricated without real bytes
and no NativeKind::SyntheticStub native is registered or invoked. It is
orthogonal to --real-jdk / --synthetic-jdk, which select which class
library boots; this selects which substitutions are permitted. One binary
runs both policies, so a failure can be A/B'd in the same shell.
This is an internal diagnostic, not a supported runtime mode. Wave 1 is
instrumentation and measurement: only class fabrication and synthetic-native
registration actually enforce, while the remaining dispatch paths are counted
rather than blocked. A program that runs fine under --real-jdk may fail under
--jdk-only — that is the signal the mode exists to produce. The default
(compatible) behaviour is unchanged, on both the launcher and embedded entry
points, and is reached by doing nothing. Normative contract:
docs/feature-designs/jdk-only-mode.md; operator guide:
docs/jdk-only-migration.md.
Added
--jdk-onlylauncher flag. ImpliesJdkMode::Realand requires a real JDK runtime image — there is no silent fallback, and the failure names the flag, the searched paths and the accepted JDK layout. Conflicts with--synthetic-jdk(that library is the set of substitutions the flag forbids), and the conflict is diagnosed as a policy error rather than a library error.- Four diagnostic flags, all usable in either mode — under the default
compatiblemode they census what strict mode would reject:--jdk-only-report <FILE>(JSON violations plus class-origin and per-NativeKindinvocation counters,schema_version1),--dump-class-origins <FILE>(see below),--trace-jdk-only(log each recorded violation to stderr), and--explain-jdk-only(long-form operator-facing explanation per violation, and leaves absolute paths unredacted in every report file; they are redacted by default). --dump-class-origins <FILE>— a new class-origin census, one row per class the class manager holds:{name, origin, reason, requested_by, real_bytes_found, loader_id}, sorted by(name, loader_id, origin)for byte-stable output, with acountsblock keyed by origin tag.- A
ClassOriginprovenance model onClass(classloading/src/class_origin.rs), replacing "is this a stub, yes or no?" with where the bytes actually came from:BootImage,ApplicationClassPath,UserDefined,VmArray,HiddenClass,GeneratedLambda,GeneratedProxy,ReflectionAccessor,VmInternal,CompatibilityStub. OnlyCompatibilityStubis rejected under the strict policy; arrays, hidden classes, lambdas, proxies and reflection accessors are products of a conforming JVM and are allowed, with their own distinct origins. The pre-existingClass::is_synthetic_stubbool is retained as a derived mirror oforigin.is_compatibility_stub()(~160 read sites across 17 files depend on it); both are written together throughClass::set_origin. - Shared policy token
cratonvm_types::compat(CompatibilityMode,ExecutionPolicy) withNativeKind::allowed_inandClassOrigin::allowed_inas predicates next to their own types, a structuredJdkOnlyViolationerror family intypes/src/error.rs, and a single policy-awareresolve_dispatch/DispatchDecisionnative-vs-bytecode decision point invm/src/vm/vm_exec.rsthat the main interpreter path now routes through. - Per-VM policy state:
VmConfig::compatibility_mode(plusis_jdk_only,execution_policy,validate_compatibility), propagated into the native registry and theClassManagerat VM init. No process globals were added for this feature. - C ABI (
libcratonvm):cratonvm_create_with_compatibility(args, mode),cratonvm_compatibility_mode(vm)(read back what the live VM actually got), andcratonvm_compatibility_mode_supported(mode)(a capability probe that needs no VM, so a host can avoid a failed create). The mode constants areCRATONVM_COMPATIBILITY_COMPATIBLE = 0andCRATONVM_COMPATIBILITY_JDK_ONLY = 1. These numeric values are a published, append-only part of the ABI — a value may be added, never renumbered — and they arecratonvm_jintrather than a boolean so a third posture can be added later without breaking a compiled host. An unrecognised value is rejected (NULL+cratonvm_last_error()), never clamped toCOMPATIBLE;cratonvm_compatibility_modereturns-1, never a mode value, on a bad handle. The option string"--jdk-only"is the second route to strict mode and the only one available toJNI_CreateJavaVM; passingCRATONVM_COMPATIBILITY_COMPATIBLEalongside--jdk-onlyis a contradiction error, not a precedence rule. - A 21-vector strict regression corpus (
regression-suite,SUITE=jdk-only) indexed against the blocker rows indocs/jdk-only-runtime-services.md, and an advisoryjdk-onlyCI job that runs the censuses. Some strict-mode vectors are expected to fail while fabrication enforcement is incomplete: that is the enforcement test working, not a regression.
Changed
--dump-native-registryoutput format changed (consumer-visible). The native census now emits"schema_version": 2; the previous output carried noschema_versionkey at all, so any consumer that parsed the old shape needs updating. Eachnatives[]entry gainsregistered_by(the registration site, captured via#[track_caller]),overwrote(theNativeKindof the entry this registration replaced, if any — registration is last-write-wins), andinvocations(times the slot was dispatched this run). Areal_declaring_methodfield is present and isnullon every row today; filling it in needs a non-initiating probe of the runtime image, because resolving it at shutdown through ordinary class loading would load classes the run never touched and change the very census the file reports. A top-level"invocations"block gives the per-NativeKinddispatch totals. Rows are sorted by(class, name, descriptor, registered_by)—registered_byis part of the key because a superseded row and the row that overwrote it share the triple. Absolute paths inregistered_byare redacted unless--explain-jdk-onlyis passed.NativeMethodRegistrygained VM-scoped policy (set_compatibility_mode,compatibility_mode), a refusal log (refused_registrations), aschema_version2 census (census), and hot-path-safe invocation counting (record_invocation,invocations_of_kind) that does not require&mut self. UnderJdkOnly,register()refuses to insert aSyntheticStuband records aSyntheticNativeRegisteredviolation instead.
Deprecated
CRATONVM_REAL=-stubsin favour of--jdk-only. The env token keeps working unchanged as a native-registry filter, but it now prints a one-time note recommending--jdk-only: the token can only drop stub registrations, and cannot express the class-loading or dispatch half of the policy. Strict mode is deliberately never inferred fromCRATONVM_REAL/CRATONVM_NO_STUBS, from a Cargo feature, or from what the host machine has installed — a run must not end up enforcing rules nobody asked for.
Known follow-ups
- The residual synthetic-stub set is unchanged:
native-builtins/tests/stub_ratchet.rsstill freezesBASELINE_SYNTHETIC_STUBS = 157exactly withSLACK = 0, and the end-statestrict_mode_refuses_nothingtest is deliberately#[ignore]d until that baseline reaches zero. Wave 1 refuses those registrations under--jdk-only; it does not retire them. The path is reclassification first, deletion second — a previous global drop was reverted the same day it landed. - The wave-2 backlog is ranked by danger in
docs/known-issues/jdk-only/README.md; its first tier causes silent wrong behaviour rather than clean failure. Summarised with staging gates in ROADMAP.md.
2026-07-11 GPU offload — first real-hardware validation and feature completion
First systematic validation of the GPU offload stack on real hardware (RTX
2060, sm_75, CUDA driver 591.86, --features gpu-driver). Two passes the same
day: a morning validation run that found and fixed two dispatch-correctness
bugs, and an evening feature wave that closed most of the follow-ups the
morning pass turned up. See
docs/known-issues/gpu-offload-followups-20260711.md for full detail and
remaining open items.
Fixed (morning validation pass)
- Offload-eligible
invokestaticcall sites were being promoted into the interpreter's invoke cache after their first dispatch (or first per-call--gpu-min-workrejection), permanently bypassing the GPU offload hook on every later call at that site — a cached target dispatches straight to the CPU body and never re-enterstry_dispatch. Fixed by never promoting aHandled/HandledWithValue/FallThroughKeepHookedsite into the invoke cache (vm/src/runtime/offload.rs,DispatchOutcome). - A failed kernel's bounds-check
failure_flagwas drained after the kernel's array writebacks, so a bounds-check failure let partially-corrupted device state copy into the Java heap before the failure was observed — violating the documented "the interpreter observes no partial GPU state on kernel failure" guarantee. Fixed by drainingFailureFlagwritebacks first regardless of push order (vm/src/runtime/offload.rs::finalize_submission). - Benchmarked the fixed dispatch path against HotSpot JDK 25 (C2) and TornadoVM 4.0.1 (PTX backend) on an RTX 2060, checksums matching HotSpot bit-for-bit at every size: div-chain kernel (48 data-dependent integer divisions/element, unvectorizable on x86) 204–235× over the best CPU; 96-multiply-add kernel (a shape HotSpot C2 can auto-vectorize) ties or beats vectorized HotSpot C2 and outruns TornadoVM ~2× on the same kernel. See
bench-gpu/results/and the README "GPU offload benchmarks" section.
Added (evening feature wave)
- Transparent offload for integer/long reduction kernels (
)I/)J-returning methods, e.g.sum += a[i]*b[i]) — the interpreter's void-return-only dispatch gate is lifted for proven reductions, with the scalar result pushed onto the operand stack.)F/)Dreductions stay CPU-only by design (GPU float atomic-add is not bit-identical to Java's sequential fp accumulation). Found and fixed in the process: the reduction PTX epilogue emitted the 2-operandatom.global.addform, whichptxasrejects with "Arguments mismatch" — every reduction kernel had been silently failing module load and falling back to CPU since the epilogue was written; fixed to the 1-operandred.global.addaccumulate form, with newptxasround-trip tests added for all six lowering shapes (jit-cuda/src/lowering.rs). Measured (RTX 2060, N = 2²⁴,bench-gpu/GpuDotBench.java, checksum bit-exact): CratonVM-GPU 18 ms vs CratonVM-CPU 76 ms vs HotSpot C2 7 ms — the GPU beats CratonVM's own CPU 4.2× but not vectorized HotSpot C2 at this size (the kernel is PCIe-bound plus single-cell atomic contention); the value is completing the transparent-offload surface for a reduction shape TornadoVM 4.0.1's own PTX backend currently throwsTornadoInternalError: unimplementedon (bench-tornado/TornadoDotBench.java). - Offload eligibility and lowering for
ldc/ldc_w/ldc2_wconstant-pool loads (int constants outsidesipushrange, and any float/double/long literal) — previously any such constant killed eligibility for the whole method. Measured:bench-gpu/GpuLdcBench.java(96-step multiply-add chain, N = 2²⁴) warm 8 ms on GPU vs ~2,000 ms CPU-bound before, sample bit-exact vs HotSpot. - A curated
Math/StrictMathGPU-intrinsics table under the existingALLOW_INTRINSIC_CALLSadmission hint —sqrt(double),abs/min/max(int/long/float/double, NaN- and signed-zero-correct per Java's contract),fma(float/double) — replacing the previous analyzer hole where anyinvokestaticwas admitted but the emitter had no lowering for any of them, so every such method silently blacklisted itself to the CPU.sin/cos/exp/log/poware deliberately excluded: PTX only offers.approxtranscendentals, which would silently violate Java'sMath/StrictMathprecision contract. Seedocs/gpu/annotations.md. frem/drem(IEEE remainder) lowering, gated behind the existingALLOW_DIV_BY_ZEROadmission hint (reused rather than adding a new hint for one opcode pair); exact only for bounded quotients.lcmp/fcmpl/fcmpg/dcmpl/dcmpgvalue-form lowering (bit-exactsetp/selpsequences, correct NaN-result asymmetry between thel/gvariants). A compare that feeds a branch still rejects at the branch opcode, so no new false eligibility was introduced.- Non-zero-start counted loops (
i = K; i < bound; i++withK >= 0,Ksourced from anldc). - A JIT-caller admission gate (
vm/src/runtime/offload_jit_gate.rs) that denies JIT/OSR compilation of any caller method containing an offload-eligible call site while--gpuis active, wired into all 5 JIT/OSR admission checks in the interpreter — closes the "a hot caller's OSR silently degrades offload back to CPU" structural gap. Hardware-validated: 100 hot repetitions of a caller loop at N = 2²² hold steady at 2 ms warm per call. dispatch_asynclaunch-configuration fixes: the thread-count floor no longer clamps every launch to a minimum of 2²⁰ threads (the real per-call array length wins when known), and block size now comes fromcuOccupancyMaxPotentialBlockSizeinstead of a fixed 256-thread block.- Async API surface: a non-blocking
poll_submission_statusnow backsNative.futureIsDone/futureStatus(a real device probe via a best-effortcuLaunchHostFunchost callback, falling back to non-blockingEvent::query/cuEventQuery), finalizing a submission inline the moment the device reports done —isDone()returningtruenow means the submission is really finalized, not just "probably."Native.futureGetResultnow surfaces real scalar reduction results (boxed asInteger/Long/Float/Double) from the real submission registry instead of only the pre-Phase-6 synthetic stub map.GpuExecutor's default CUDA stream is now real and shared across an executor's submissions instead of a fresh private stream per dispatch (resolve_or_create_default_stream);newStream()also now mints a genuine CUDA stream, though nothing yet routes a dispatch onto it explicitly.GpuArray.allocate's Rust-side native shims (arrayAllocateInt/Long/Float/Double) landed; thecraton-gpu-javajar binding is still pending.i8/i16bulk array marshalling.--print-gpu-decisionsis now self-sufficient — it no longer requires a separateRUST_LOG=infoto see any output. .github/workflows/gpu-selfhosted.yml+bench-gpu/ci-gate.sh— weekly self-hosted-GPU-runner CI scaffolding for thebench-gpu/benchmark suite (checksum-verified); runner enrollment against the workflow's runner label is still pending.
Known follow-ups
GpuFuturecompletion is now poll-driven but still not push-driven:isDone()/getNow()do a real non-blocking device check and finalize inline, but nothing drives that check without an application thread calling it — no background thread or driver callback completes a future on its own yet.- 2-D/nested loops and general (non-loop-guard) branches are still rejected by the analyzer;
)F/)Dreductions remain CPU-only by design. - Full open-items list in
docs/known-issues/gpu-offload-followups-20260711.md.
2026-06 multi-agent review remediation
A second, larger review-driven remediation pass (one Opus agent per finding, merged in severity order with a build gate) closed the full critical/high/medium tier plus perf and features. Highlights:
Security
SecureRandomnow draws from the OS CSPRNG (BCryptGenRandom/getrandom) instead of an invertible splitmix64 DRBG (native-builtins/src/crypto_impl.rs); RSA private-key ops gained base blinding.- SSRF: the always-on cloud-metadata/link-local block now unwraps IPv4-mapped/compatible IPv6 (
::ffff:169.254.169.254) (native-io/src/outbound_policy.rs); optional outbound-hostname DNS resolution closes the alias/rebind bypass. - Built-in HTTP server honors
Transfer-Encoding: chunked(request-smuggling/body-desync fix); HTTP client stripsAuthorization/Cookieon cross-host redirects (native-builtins/src/{net_phase_e,http_client}.rs). X509Certificate.verifyfails closed;Class.forNamerejects control-byte/separator/..injection; AOT cache integrity moved to SHA-256.- New sandbox/egress knobs documented in
docs/SECURITY_HARDENING.md.
Soundness (GC / JIT / memory safety)
- Closed the JIT/GC "register-resident root" use-after-free family: a uniform native-root registry (
vm/src/memory/native_roots.rs) + per-subsystem scan/remap for native collections overlays, NIO selector keys, ScheduledThreadPoolExecutor runnables, XNIO IoFutures, the ClassFileTransformer chain, theObjectStreamClasscache, and value-stack smuggled jobjects; JIT x64 now spills callee-saved operand-stack oops at safepoints. - JNI: implicit local-reference frame around native calls + a refcounted GC pin set for
GetPrimitiveArrayCritical/Get*ArrayElements(gc/src/pinned.rs). - CompactHeader forwarding pointers no longer truncate above 4 GB; per-thread SATB buffers are drained at remark; concurrent-mark 16-byte slot reads are stripe-locked; ZGC backend runs reference processing.
vm-execJNI TLS cleanup is RAII (panic-safe);<clinit>failure no longer leaks the init claim; libcratonvm hands out validated opaque handles instead of raw heap pointers.
Correctness
- Bytecode verifier rejects unverified
jsr/retby default;ldc/invokespecialverifier-model fixes. BigInteger.modPow/modInversehonor signs and throw on non-invertible input;AtomicXFieldUpdaterRMW ops no longer lose updates;AbstractStringBuilder.getCharsbounds-checks; interpreter runsfinally/catch-all on JIT-unknown-PC unwind.- JNI
DefineClassdefines from the supplied buffer;Call*MethodV/Call*MethodAimplemented.
Performance
- Thread-local scratch buffer for socket read/write (no per-syscall
Vec); O(1) maps for JNI global refs, unified-logging handles, and the regex cache; bounded JIT code-cache + deopt history; metaspace bump fast-path.
Features
- Advisory
cargo-llvm-covcoverage workflow (.github/workflows/coverage.yml,docs/COVERAGE.md). - Container/cgroup-aware default heap sizing (
vm/src/runtime/container.rs,docs/CONTAINER.md). README.mdforlibcratonvmandcratonvm-embed(crates.io pages); embedding guide (docs/EMBEDDING.md).- Five L/XL design docs under
docs/feature-designs/(precise-JIT-maps-default, deopt/OSR, concurrent-GC maturation, foreign-thread attach, differential fuzzer).
Build / OSS
- MSRV raised
1.77→1.80(Cargo.toml,clippy.toml) to match the std APIs the code already uses;gc/reader/craton-gpuclippy cleaned. - Untracked the gitignored
bench/build artifacts and straydd1.out(kept on disk); test-fixture.classfiles retained. - Crate-count references corrected to 20 workspace members (
libcratonvm,cratonvm-embed, andcratonvm-difftestpresent;fuzz/remains standalone);docs/CRYPTO_STATUS.mdreclassified PBKDF2/ML-KEM/DESede as implemented.
Earlier review round
A cross-crate review-driven fix orchestrator landed 50+ commits across security, soundness, correctness, and OSS-distribution hygiene. Highlights:
Security
- JEP-290
ObjectInputFilterhonored withmaxdepth/maxrefs/maxbytes/maxarraycaps (native-builtins/src/object_input_filter.rs). - JAR signer chain verified against the JCE/JDK trust store before classes load (
classloading/src/jar_signer.rs). - Panama / FFI host calls gated behind
--enable-native-access; unauthorized callers throwIllegalCallerException(native-builtins/src/panama_*.rs). ProcessBuilder.startand Panama host calls now consultSecurityManager.checkExec(native-builtins/src/process.rs).- Test-only TLS certs and keys moved behind
cfg(test)so they cannot ship in release artifacts (native-builtins/src/tls_test_certs.rs). - Outbound network calls (HTTP/Socket/URL) run through an SSRF policy hook with a per-connect timeout (
native-io/src/net.rs). RandomAccessFile,WatchService, andProcessBuildernow route paths throughvalidate_pathbefore opening (native-io/src/*,native-builtins/src/process.rs).- New libfuzzer targets cover classfile reader, JImage parser, PKCS#12 keystore, and JAR signer (
fuzz/fuzz_targets/). vmidentity-validates resolution-cache keys so a forged class identity cannot poison lookups (vm/src/runtime/resolution_cache.rs).vmverifier-skip path is now gated on the bootstrap classloader identity, not just the loader pointer (vm/src/runtime/verifier_gate.rs).
Soundness
- SATB pre-barrier wired at remaining
aastore/putfieldsites plus a real stop-the-world fornewarray(vm/src/runtime/interpreter.rs,jit/src/runtime_helpers.rs). gcmutating heap entry points now require aStopTheWorldTokenwitness (gc/src/lib.rs).- Async-signal-safe SIGSEGV handler installed on Unix (no allocations, no locks) (
vm/src/runtime/signals.rs). - AArch64 icache flush on Linux and FreeBSD after JIT code emission (
jit/src/aarch64.rs). vmhot locks reordered throughOrderedMutexmatchingdocs/lock-order.md(vm/src/lock_order.rs).- JIT switch-target offsets are now overflow-checked;
try_patchreplaces panickingpatch_i32/patch_byte(jit/src/buffer.rs). reader::ByteView::try_newreturnsResulton overflow / misalignment instead of UB (reader/src/byte_view.rs).gcbitmap clears useAcqRelordering; theSATBwrite barrier is now part of the trait surface (gc/src/g1.rs).jfr::SpscEventRing::Dropperforms a bounded shutdown and releases pending payloads (jfr/src/ring.rs).
Correctness
- JFR field emit validates variant against declared type per event (
jfr/src/event.rs). - Native collections rekey GC overlays on
identity_hash_codeso post-GC pointer remap keeps maps consistent (native-collections/src/*). - Blocking queue park / notify discipline cleaned up with read-locks instead of unsynchronized shared state (
native-collections/src/blocking_queue.rs). - CUDA H2D → kernel → D2H now sequenced on the same stream; previous code raced (
cuda-bridge/src/stream.rs). jit-apiexposes avalidate()loop,repr(C)golden offsets, and a fixedNUM_FIELDSconstant for ABI lock-in (jit-api/src/lib.rs).types::CompactValue::update_object_ptrreturnsResult, andas_long_uncheckeddocuments its lazy-decode invariant (types/src/compact_value.rs).native-builtins--enable-native-accessaudit; Panama host calls check the caller module against the allow-list.readerattribute shape validation propagates the signature depth-guard "sticky" flag (reader/src/attribute.rs).native-builtinsJCA crypto routes AES / AES-GCM throughaes/aes-gcmRustCrypto (constant-time).vm-clirebuilt for HotSpot-Xmx/-XXparsing,--nojit,String[] args(vm-cli/src/main.rs).native-api::allocateno longer leaks on the error path;init_levelis monotonic;tcp_availableno longer clobbers state (native-api/src/lib.rs).
OSS / Distribution
vm-cliproduces thecratonvmbinary by default; thejava[.exe]alias is opt-in via--features java-bin-aliassocargo installdoes not shadow a real JDK (vm-cli/Cargo.toml).- Added
SUPPORT.md,GOVERNANCE.md,MAINTAINERS.md,THIRD-PARTY-NOTICES.md, and a GitHub issue-template config (top-level +.github/). - SPDX
Apache-2.0headers on every Rust source file across the workspace. - MSRV bumped to 1.77 and synchronized across
README.md,BUILD_GUIDE.md,CONTRIBUTING.md, anddocs/INSTALL.md. - Workspace version raised to
0.3.0; every inter-cratepath = "../<crate>"declaration now carriesversion = "0.3.0"socargo publish --dry-runaccepts the manifest. - Per-crate
README.mdadded for crates.io rendering across the then-current publishable crates and tooling crates. fuzz/has its own standalone nightly-only workspace and remainspublish = false.- Workspace crate-count references were aligned in
README.md,ARCHITECTURE.md, andBUILD_GUIDE.md; later workspace additions bring the current count to 20. - CI parked workflows reactivated with
clippy -D warningsas a hard gate (.github/workflows/ci.yml).
Known follow-ups
- Re-enable JIT loop unrolling — previous byte-copy unrolling produced corrupt native code and was disabled (
jit/src/x64/unroll.rs). - Real-JDK boot via
java.baseJMOD remains opt-in; synthetic stubs cover the default path. - Concurrent GC marking is still serialized under STW; G1 / ZGC remain experimental.
0.3.0 - 2026-05-24
Added
- Real cryptographic signature verification in
x509_manager::validate_chainfor RSA-SHA256 (PKCS#1 v1.5) and ECDSA-with-SHA256 over P-256, replacing the previous structural-only "signature present" check. DSA-with-SHA1, RSA-PSS, and Ed25519 now reportTrustError::NotImplemented { oid }so callers can choose to delegate to JCE. - JIT XMM register allocation for float/double locals (callee-saved XMM8-XMM15 on Windows x64), eliminating frame spills for FP-heavy methods.
- JIT
Math.sqrtintrinsic inlined asSQRTSDinstead of going through interpreter dispatch. - JIT
dup2opcode support, enabling compound array assignments likea[i] += x. - JIT
ldc2_wopcode support for loading long/double constants from the constant pool. - JIT OSR trampoline now transfers float/double locals into their assigned XMM registers.
- JIT
getstaticcaching: unique static field values are loaded once in the method prologue and cached in frame slots. - JIT
StackSlot::Xmmoperand-stack variant so consecutive double operations chain in XMM registers without memory traffic. - Extracted 10 crates from the monolithic vm: classloading, gc, jit, jit-api, types, native-api, native-builtins, native-collections, native-io, jfr.
- G1 and ZGC garbage collectors.
- AArch64 JIT backend (partial; 45% of x86-64 opcode coverage).
- Java Flight Recorder support.
- JVMTI event framework.
- Security hardening: checked arithmetic throughout GC and JIT.
Changed
- MSRV bumped to 1.77 (was 1.75).
- Updated benchmark numbers against JDK 25.0.1 C2: QuickBench 1.50x, Fannkuch 1.57x, N-Body 20x (down from 464x interpreter-only).
- Added Binary Trees (CLBG) benchmark, exposing a GC allocation bottleneck (23.3x ratio).
- N-Body and Fannkuch-Redux benchmarks now run to completion with correct results.
- Rewrote roadmap with an honest production-readiness evaluation distinguishing real working features from Rust-side stubs.
- New tiered priority matrix (Tier 0 basic correctness through Tier 3 production grade) with measurable success metrics verified against real Java code.
Fixed
- VM-generated exceptions (NPE, AIOOBE, ArithmeticException, ClassCastException, etc.) are now catchable by Java
try/catchinstead of being Rust-side errors that bypassed exception handling. HashMap.entrySet()iteration: synthetic inner-class types likeHashMap$Entrynow satisfycheckcast/instanceofagainstMap.Entry,Iterator,Iterable,Collection, andComparable.Thread(Runnable)andThread(String)constructors are now registered;thread.start()works as an alias forstart0().Class.getName()andClass.getSimpleName()are now registered.java.io.FileWriterconstructors and write methods are registered, including append mode andFile-path overloads.- JIT-compiled methods returning
boolean/byte/char/short/float/doublenow return the correct value instead of being treated asvoid. - JIT call dispatch now preserves
floatanddoubleargument bit patterns (previously collapsed to 0). - JIT invoke dispatch now installs the thread context before executing compiled code, fixing
invokevirtual/invokeinterfacereturning 0. Stream.filter(...).count()andstream().filter(...).collect(...)now return correct results (previously returned 0 or stack-overflowed).- JIT register allocator rewritten to use instruction-level liveness, fixing Fannkuch miscompilations where two locals shared a register.
- JIT operand-stack canonicalization at forward-branch targets and dead-to-live transitions, fixing miscompilation on complex control flow.
- JIT
ifeq..iflenow usesTESTinstead ofCMP reg,reg, correctly setting flags. - JIT
if_icmpXXcodegen optimized to use direct register comparison. - N-Body segfault root-caused to loop unrolling producing corrupt native code; N-Body now runs cleanly with unrolling disabled.
- JIT loop unrolling re-enabled behind a byte-copy-safety predicate. The byte-copy unroller is only correct when every opcode in the body is position-independent (or one of the rel32 patch flavours the duplicator now handles, namely
forward_patches,bounds_check_stubs, andnull_check_store_stubs). Bodies containing field/static accesses, invokes, allocations, throws, instanceof/checkcast, monitor ops, switches, or any other helper-call opcode are skipped. SetCRATONVM_UNROLL_UNSAFE_BODIES=1to re-enter the legacy unguarded path for bisection. - Integer truncation in array allocation (security).
- Unchecked branch offsets in JIT (security).
- Path traversal in resource loading (security).
- StringBuilder
insert()O(n^2) performance regression. - Bytecode verifier now accepts
InterfaceMethodrefforinvokestatic/invokespecial(Java 8+ static interface methods). SSLEnginehandshake state machine:wrap/unwrap/beginHandshaketransitions.- Crypto
deriveKey/deriveDatanow call the HKDF implementation instead of returning empty output. - File descriptor leak in
fd_table: rollback on overflow,close()returnsResult. - Serialization write methods now throw
UnsupportedOperationExceptioninstead of silently succeeding. - JIT negative cache: failed compilations are no longer re-attempted on every invocation.
vm-cliargs-array error handling usesmap_errinstead ofwith_contexton non-Errortypes.
Performance
- GC
alloc_arrayno longer double-zeroes the data region; the redundant memset after young-gen allocation is removed. - GC young-gen mutex is released before the zero-init memset, so large-allocation latency no longer holds the global allocation lock.
- N-Body FP arithmetic improved from 464x to 20x vs JDK 25 C2 via XMM stack slots,
Math.sqrtintrinsic, and OSR XMM transfer.
Known Issues
- JIT loop unrolling is now gated on a byte-copy safety predicate (above); pure-arithmetic and array-index-store kernels are unrolled, but loops with field accesses or invokes still execute unrolled-by-1 until the duplicator learns to clone deopt/exception/MIC/PIC stubs.
- BigDecimal/BigInteger arithmetic on post-clinit-populated statics returns 0 (
BigDecimal.ONE.add(BigDecimal.TEN)yields 0). Boot paths that only reference these values work; numeric workloads (JDBC numeric, Jackson numeric) do not. ForkJoinPool.invoke(RecursiveTask)at recursion depth >= 10 returns 0 due to a JIT register clobber in deeply-recursive boxed-Longarithmetic. Workaround: disable the JIT for affected workloads.- GC throughput is roughly 23x slower than JDK on allocation-heavy workloads (Binary Trees).
0.2.0 - 2025-06-01
Added
- x86-64 JIT compiler with 26 optimization rounds (~140 bytecodes compiled)
- AVX2 SIMD vectorization for integer reduction loops
- On-Stack Replacement (OSR) at hot loop back-edges
- Loop-Invariant Code Motion (LICM)
- Array Bounds Check Elimination (BCE)
- Magic number division (no IDIV)
- SSE float/double arithmetic pipeline
- SoA (Structure-of-Arrays) value layout for 44% memory reduction
- Generational garbage collector with write barriers and card table
- Multi-threading with monitors, ReentrantLock, CountDownLatch, Semaphore, CyclicBarrier
- Virtual threads (simplified carrier-based scheduler)
- Java 11 support: nest-based access control (JEP 181)
- Java 17 support: records (JEP 395), sealed classes (JEP 409)
- Java 21 support: pattern matching for switch, sequenced collections
- Java 25 support: stream gatherers, scoped values, structured concurrency
- Panama FFI: MemorySegment, Arena, ValueLayout, SymbolLookup, Linker (downcall/upcall)
- 3,100+ native method registrations across java.lang, java.util, java.io, java.time, java.nio
- Full reflection: Class.forName, Method.invoke, Field.get/set, Constructor.newInstance
- Lambda/invokedynamic via LambdaMetafactory and StringConcatFactory
- CONSTANT_Dynamic (condy) support
- Enhanced NPE messages (JEP 358)
- Hidden classes (JEP 371)
- Partial JNI function table (229 slots, 13 implemented)
- Module system basics: Module, ModuleDescriptor, ModuleLayer
- Class file versions 45-69 (Java 1.1 through Java 25)
- Dependabot for automated dependency updates
- CODEOWNERS for review routing
- GitHub Security Advisories for private vulnerability reporting
- ARCHITECTURE.md for contributor onboarding
- Release workflow for automated binary builds
Changed
- Improved SAFETY documentation on unsafe blocks in heap allocator
- Added checked allocation methods (
alloc_object_checked,alloc_array_checked) - Replaced test
panic!()calls with properassert!macros in GC and JIT tests - Updated test documentation references across the public docs.
Performance
- Within 1.41x of JDK 25 C2 on QuickBench overall
- Fibonacci(42): 1.07x — within 7% of C2
0.1.0 - 2025-01-15
Added
- Bytecode interpreter with 200+ JVM instructions
.classfile parser supporting all standard attributes- Command-line launcher with classpath and heap size configuration
- CI pipeline with cross-platform testing (coverage and Miri jobs scaffolded but planned, not yet enabled)