JNIEnv::{push,with}_local_frame
can cause JObject
s to be freed before their lifetime ends, potentially resulting in use-after-free bugs and undefined behavior.
Proof of concept:
use jni::{*, objects::*};
fn main() {
// Initialize JVM.
let jvm = JavaVM::new(
InitArgsBuilder::new()
.version(JNIVersion::V8)
.option("-Xcheck:jni")
.build()
.unwrap()
).unwrap();
let env = jvm.attach_current_thread().unwrap();
// Construct an object inside a local reference frame, but leak the reference out of that frame.
let mut obj: Option<JObject<'_>> = None;
env.with_local_frame(2, || {
obj = Some(env.new_object(
"java/lang/Integer",
"(I)V",
&[42.into()],
).unwrap());
Ok(JObject::null())
}).unwrap();
// Try to call a method on it. This will crash.
let answer = env.call_method(
obj.unwrap(),
"intValue",
"()I",
&[],
).unwrap().i().unwrap();
println!("The answer is {answer}.");
}
The lifetimes of JObject
s are not limited by local reference frames. They have the lifetime of the JNIEnv
that created them, which is longer than the lifetime of a local reference frame created with that same JNIEnv
.
This seems difficult to fix. My first thought was to make {push,with}_local_frame
take a &mut self
parameter (and make JNIEnv
!Copy
), but then it would be impossible to have any live JObject
s outside of the local frame, because Rust considers the JNIEnv
already borrowed.
We could make all local references (JObject
, JClass
, etc) behave like AutoLocal
, that is, non-Copy
and free-on-drop. Then we make {pop,push,with}_local_frame
unsafe
(or just remove them, since this would make local reference frames mostly unnecessary).
Additional benefits:
-
This would solve the safety issues with AutoLocal
described in #381, since there would be no (non-unsafe
) way to have more than one JObject
wrapping the same local reference.
-
This would fix the memory leak described in #109, since JClass
is now freed upon drop too.
-
JNIEnv::delete_local_ref
, which also can free JObject
s before the end of their lifetime, would no longer be necessary and could be removed.
Drawbacks:
-
This would be a pretty extensive change. Most parameters of type JObject
would need to be changed to &JObject
, in both this crate and user code, since it's no longer Copy
. Similarly, trait bounds like T: Into<JObject>
would have to be changed, perhaps to T: AsRef<JObject>
(and add impl<'a> AsRef<JObject<'a>>
for JObject<'a>
and every type that wraps it).
-
JObject
and friends no longer implement Copy
or Clone
. From now on, the only way to duplicate a JObject
is with JNIEnv::new_local_ref
. I'm not sure whether that would be a nuisance or not, but if it is, then we could make it less of one by adding a new method pub fn clone<'b>(&self, env: JNIEnv<'b>) -> Result<_<'b>>
to each of these types.
What do you think? Would you like me to do this?