This document captures the safety model for jvmti-bindings and a checklist to review before shipping an agent.
- Treat all JNI/JVMTI callbacks as
unsafeboundaries. - Never panic across a JNI/JVMTI callback boundary.
- Do not store or share
JNIEnvacross threads. - For embedded JVMs, only use
creator_envon the creating thread; attach/detach other threads. - Use
GlobalReforWeakGlobalReffor long-lived references and ensure they are released. - Always check JVMTI error codes and handle failures explicitly.
- Assume callbacks can be concurrent and re-entrant.
- Avoid long-running work inside callbacks; offload to worker threads.
- Respect callback-specific constraints (some callbacks forbid JNI).
- Keep unsafe operations narrow. The crate denies
unsafe_op_in_unsafe_fn, so an unsafe function does not implicitly make its whole body an unsafe block. - Keep a dynamically loaded JVM library alive until every function pointer obtained from it is no longer callable and the embedded VM is destroyed.
- Capabilities requested in
on_loadmatch the events you enable. - Event callbacks are registered before enabling notifications.
JNIEnvis only used on the thread that provided it.- No
unwrap()or panics in callback code paths. - JVM TI allocations remain in
JvmtiAllocationguards, or raw ownership is explicitly transferred and later released with unsafeJvmti::deallocate_rawon the same environment. - Owning reference guards are closed or dropped; raw JNI/JVM TI object returns are explicitly deleted or bounded by a local frame, especially on long-running agent threads.
- Agent state is thread-safe (
Mutex, atomics, or lock-free). - You avoid JNI calls during
GarbageCollectionStart/Finishcallbacks. - Native method redirects or bytecode rewriting are validated and bounded.
- Any attach-based initialization is idempotent.
- Never treat JNI, JVM TI, or
CONSTANT_Utf8bytes as ordinary UTF-8. Native strings are NUL-terminated Java Modified UTF-8; use themutf8APIs and use UTF-16 when unpaired Java surrogates must be preserved. - Treat pointer lifetimes as scoped to the callback unless documented otherwise.
- Validate lengths before copying into Rust buffers.
- Prefer owned Rust structures over returning raw JVMTI structs.
- Check runtime feature support before touching an appended JNI tail, a reclaimed JVM TI slot, or a newly consumed capability bit.
- Treat null allocation objects as valid for JDK 28 value-object events.
- Keep JNI array-element and critical-region leases in their owning guards.
abort()cannot undo writes to directly pinned Java storage. - Do not block or invoke arbitrary JNI functions while a critical-region guard is live.
- Prefer
LocalFrameandJavaMonitorGuard; manually paired local frames and monitor entry are unsafe*_rawoperations. - Prefer
&CStrAPIs for fixed JNI names in hot paths; construct or validate the C string outside the callback loop. - For dynamic symbols, verify platform loader success before converting an address to a typed function pointer. The caller remains responsible for the symbol's exact ABI and signature.
- Assume
Agent_OnAttachcan be called repeatedly and concurrently. Do not reconstruct process-global state or treat a second attach as impossible. - Prefer owning
RawMonitor/RawMonitorGuardvalues. If explicitcloseorexitfails, ownership remains live and drop makes one best-effort retry. - Treat
audits/unsafe-surface-3.0.txtas a review tripwire, not a proof of soundness. Any change requires the independent checklist inUNSAFE_FFI_REVIEW.mdbefore its baseline is regenerated. - Call
unsafe Jvmti::dispose_environmentonly after disabling and draining every callback for that environment. The JVM may let already-running callbacks continue, and no environment-derived pointer or wrapper may be used after disposal.
The exported lifecycle and callback trampolines use catch_unwind as defense
in depth. That contains only unwinding panics. With panic = "abort", a panic
terminates the process before the library can intercept it. No callback should
use panic as normal error handling under either strategy; return a status from
lifecycle methods and record or defer errors from void event callbacks.
Prefer attach_current_thread_guard or the scoped
with_attached_current_thread helpers. The manual get_env, attach, and detach
methods are unsafe because Rust cannot otherwise prevent a JniEnv from
outliving the VM, crossing threads, or being used after detachment.