Repository: jni-rs/jni-rs (Rust)

Issue:
`{get,set}[_static]_field_unchecked` should be `unsafe`
The `Get<Type>Field` and `Set<Type>Field` families of JNI methods are not specified to perform a check on the given object's type. This means that calling the method on an object of the wrong type may access out of bounds memory.
JNIEnv::set_static_field looks like it can be used unsafely
It looks like the current implementation of `set_static_field` should be marked `unsafe` since it doesn't lookup the field ID based on the string name and signature.

It's currently possible to directly pass a `JStaticFieldID` that's might not be valid for the given `class` and might not have a type that matches the given `value`.

To make it safe it should probably take an additional `ty`, `AsRef<JNIStr>` argument like `set_field`  does, so then the implementation can ensure that the field ID is looked up internally.

I noticed this when looking into #505


Discussion:
Right, thanks.

It looks like `set_static_field` as it is currently should also be `unsafe` since it doesn't lookup the field ID based on the string name and signature (so it's possible to directly pass a `JStaticFieldID` that's not valid for the given class).

`set_static_field` should probably take an additional `ty` or `sig` argument that's `AsRef<JNIStr>`, similar to `set_field` so it can ensure the field ID is resolved on-the-fly and is guaranteed to be valid.





Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 69854a1f..dac580c0 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -49,7 +49,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 - `JValueGen` has been removed. `JValue` and `JValueOwned` are now separate, unrelated, non-generic types. ([#429](https://github.com/jni-rs/jni-rs/pull/429))
 - Make most `from_raw()`, `get_raw()` and `into_raw()` methods `const fn`. ([#453](https://github.com/jni-rs/jni-rs/pull/453))
 - `get_object_class` borrows the `JNIEnv` mutably because it creates a new local reference. ([#456](https://github.com/jni-rs/jni-rs/pull/456))
-- `get/set_field_unchecked` have been marked as unsafe since they can lead to undefined behaviour if the given types don't match the field type ([#457](https://github.com/jni-rs/jni-rs/pull/457))
+- `get/set_*_field_unchecked` have been marked as unsafe since they can lead to undefined behaviour if the given types don't match the field type ([#457](https://github.com/jni-rs/jni-rs/pull/457) + [#629](https://github.com/jni-rs/jni-rs/pull/629))
+- `set_static_field` takes a field name and signature as strings so the ID is looked up internally to ensure it's valid. ([#629](https://github.com/jni-rs/jni-rs/pull/629))
 - `JNIEnv::get/set/take_rust_field` no longer require a mutable `JNIEnv` reference since they don't return any new local references to the caller ([#455](https://github.com/jni-rs/jni-rs/issues/455))
 - `JNIEnv::is_assignable_from` and `is_instance_of` no longer requires a mutable `JNIEnv` reference, since they doesn't return an new local references to the caller
 - `JavaStr` has been renamed `MUTF8Chars` and intended to be got via `JString::mutf8_chars()`
diff --git a/src/wrapper/jnienv.rs b/src/wrapper/jnienv.rs
index bb3786b2..e1c3eb01 100644
--- a/src/wrapper/jnienv.rs
+++ b/src/wrapper/jnienv.rs
@@ -2978,17 +2978,19 @@ impl<'local> JNIEnv<'local> {
     /// Get a field without checking the provided type against the actual field.
     ///
     /// # Safety
-    /// There will be undefined behaviour if the return type `ty` doesn't match
-    /// the type for the given `field`
-    pub unsafe fn get_field_unchecked<'other_local, O, T>(
+    ///
+    /// - The `obj` must not be `null`
+    /// - The `field` must be associated with the given `obj` (got from passing the `obj` to [JNIEnv::get_field_id])
+    /// - The field must have the specified `ty` type.
+    pub unsafe fn get_field_unchecked<'other_local, O, F>(
         &mut self,
         obj: O,
-        field: T,
+        field: F,
         ty: ReturnType,
     ) -> Result<JValueOwned<'local>>
     where
         O: AsRef<JObject<'other_local>>,
-        T: Desc<'local, JFieldID>,
+        F: Desc<'local, JFieldID>,
     {
         // Runtime check that the 'local reference lifetime will be tied to
         // JNIEnv lifetime for the top JNI stack frame
@@ -3037,19 +3039,21 @@ impl<'local> JNIEnv<'local> {
     /// Set a field without any type checking.
     ///
     /// # Safety
-    /// There will be undefined behaviour if the `val` type doesn't match
-    /// the type for the given `field`
-    pub unsafe fn set_field_unchecked<'other_local, O, T>(
+    ///
+    /// - The `obj` must not be `null`
+    /// - The `field` must be associated with the given `obj` (got from passing the `obj` to [JNIEnv::get_field_id])
+    /// - The field type must match the given `value` type.
+    pub unsafe fn set_field_unchecked<'other_local, O, F>(
         &mut self,
         obj: O,
-        field: T,
-        val: JValue,
+        field: F,
+        value: JValue,
     ) -> Result<()>
     where
         O: AsRef<JObject<'other_local>>,
-        T: Desc<'local, JFieldID>,
+        F: Desc<'local, JFieldID>,
     {
-        if let JValue::Void = val {
+        if let JValue::Void = value {
             return Err(Error::WrongJValueType("void", "see java field"));
         }
 
@@ -3065,7 +3069,7 @@ impl<'local> JNIEnv<'local> {
             }};
         }
 
-        match val {
+        match value {
             JValue::Object(o) => set_field!(SetObjectField(o.as_raw())),
             JValue::Bool(b) => set_field!(SetBooleanField(b)),
             JValue::Char(c) => set_field!(SetCharField(c)),
@@ -3083,16 +3087,16 @@ impl<'local> JNIEnv<'local> {
 
     /// Get a field. Requires an object class lookup and a field id lookup
     /// internally.
-    pub fn get_field<'other_local, O, S, T>(
+    pub fn get_field<'other_local, O, N, S>(
         &mut self,
         obj: O,
-        name: S,
-        ty: T,
+        name: N,
+        sig: S,
     ) -> Result<JValueOwned<'local>>
     where
         O: AsRef<JObject<'other_local>>,
+        N: AsRef<JNIStr>,
         S: AsRef<JNIStr>,
-        T: AsRef<JNIStr>,
     {
         // Runtime check that the 'local reference lifetime will be tied to
         // JNIEnv lifetime for the top JNI stack frame
@@ -3100,62 +3104,62 @@ impl<'local> JNIEnv<'local> {
         let obj = obj.as_ref();
         let class = self.get_object_class(obj)?.auto();
 
-        let ty = ty.as_ref();
-        let parsed = ReturnType::from_str(&ty.to_str())?;
-
-        let field_id: JFieldID = Desc::<JFieldID>::lookup((&class, name, ty), self)?;
+        let sig = sig.as_ref();
+        let field_ty = JavaType::from_str(&sig.to_str())?;
+        let field_id: JFieldID = Desc::<JFieldID>::lookup((&class, name, sig), self)?;
 
         // Safety: Since we have explicitly looked up the field ID based on the given
         // return type we have already validate that they match
-        unsafe { self.get_field_unchecked(obj, field_id, parsed) }
+        unsafe { self.get_field_unchecked(obj, field_id, field_ty) }
     }
 
     /// Set a field. Does the same lookups as `get_field` and ensures that the
     /// type matches the given value.
-    pub fn set_field<'other_local, O, S, T>(
+    pub fn set_field<'other_local, O, N, S>(
         &mut self,
         obj: O,
-        name: S,
-        ty: T,
-        val: JValue,
+        name: N,
+        sig: S,
+        value: JValue,
     ) -> Result<()>
     where
         O: AsRef<JObject<'other_local>>,
+        N: AsRef<JNIStr>,
         S: AsRef<JNIStr>,
-        T: AsRef<JNIStr>,
     {
         let obj = obj.as_ref();
-        let ty = ty.as_ref();
-        let field_ty = JavaType::from_str(&ty.to_str())?;
-        let val_primitive = val.primitive_type();
-
-        let wrong_type = Err(Error::WrongJValueType(val.type_name(), "see java field"));
-
-        match field_ty {
-            JavaType::Object | JavaType::Array if val_primitive.is_some() => wrong_type,
-            JavaType::Primitive(p) if val_primitive != Some(p) => wrong_type,
-            JavaType::Primitive(_) if val_primitive.is_none() => wrong_type,
-            _ => {
-                let class = self.get_object_class(obj)?.auto();
-
-                // Safety: We have explicitly checked that the field type matches
-                // the value type
-                unsafe { self.set_field_unchecked(obj, (&class, name, ty), val) }
-            }
+        let sig = sig.as_ref();
+        let field_ty = JavaType::from_str(&sig.to_str())?;
+
+        if value.java_type() != field_ty {
+            return Err(Error::WrongJValueType(value.type_name(), "see java field"));
         }
+
+        let class = self.get_object_class(obj)?.auto();
+
+        // Safety: We have explicitly checked that the field type matches
+        // the value type and the field ID is going to be looked up dynamically
+        // based on the class, name and signature (so it's safe to use)
+        unsafe { self.set_field_unchecked(obj, (&class, name, sig), value) }
     }
 
     /// Get a static field without checking the provided type against the actual
     /// field.
-    pub fn get_static_field_unchecked<'other_local, T, U>(
+    ///
+    /// # Safety
+    ///
+    /// - The `class` must not be null
+    /// - The `field` must be associated with the given `class` (got from passing the `class` to [JNIEnv::get_static_field_id])
+    /// - The field must have the specified `ty` type.
+    pub unsafe fn get_static_field_unchecked<'other_local, C, F>(
         &mut self,
-        class: T,
-        field: U,
+        class: C,
+        field: F,
         ty: JavaType,
     ) -> Result<JValueOwned<'local>>
     where
-        T: Desc<'local, JClass<'other_local>>,
-        U: Desc<'local, JStaticFieldID>,
+        C: Desc<'local, JClass<'other_local>>,
+        F: Desc<'local, JStaticFieldID>,
     {
         // Runtime check that the 'local reference lifetime will be tied to
         // JNIEnv lifetime for the top JNI stack frame
@@ -3205,47 +3209,23 @@ impl<'local> JNIEnv<'local> {
         ret
     }
 
-    /// Get a static field. Requires a class lookup and a field id lookup
-    /// internally.
-    pub fn get_static_field<'other_local, T, U, V>(
-        &mut self,
-        class: T,
-        field: U,
-        sig: V,
-    ) -> Result<JValueOwned<'local>>
-    where
-        T: Desc<'local, JClass<'other_local>>,
-        U: AsRef<JNIStr>,
-        V: AsRef<JNIStr>,
-    {
-        // Runtime check that the 'local reference lifetime will be tied to
-        // JNIEnv lifetime for the top JNI stack frame
-        assert_eq!(self.level, JavaVM::thread_attach_guard_level());
-        let sig = sig.as_ref();
-        let ty = JavaType::from_str(&sig.to_str())?;
-
-        // go ahead and look up the class sincewe'll need that for the next
-        // call.
-        let class = class.lookup(self)?;
-
-        self.get_static_field_unchecked(class.as_ref(), (class.as_ref(), field, sig), ty)
-    }
-
     /// Set a static field. Requires a class lookup and a field id lookup internally.
-    pub fn set_static_field<'other_local, T, U>(
+    ///
+    /// # Safety
+    ///
+    /// - The `class` must not be null
+    /// - The `field` must be associated with the given `class` (got from passing the `class` to [JNIEnv::get_static_field_id])
+    /// - The field type must match the given `value` type.
+    pub unsafe fn set_static_field_unchecked<'other_local, C, F>(
         &mut self,
-        class: T,
-        field: U,
+        class: C,
+        field: F,
         value: JValue,
     ) -> Result<()>
     where
-        T: Desc<'local, JClass<'other_local>>,
-        U: Desc<'local, JStaticFieldID>,
+        C: Desc<'local, JClass<'other_local>>,
+        F: Desc<'local, JStaticFieldID>,
     {
-        if let JValue::Void = value {
-            return Err(Error::WrongJValueType("void", "see java field"));
-        }
-
         let class = class.lookup(self)?;
         let field = field.lookup(self)?;
 
@@ -3283,6 +3263,62 @@ impl<'local> JNIEnv<'local> {
         Ok(())
     }
 
+    /// Get a static field. Requires a class lookup and a field id lookup
+    /// internally.
+    pub fn get_static_field<'other_local, C, N, S>(
+        &mut self,
+        class: C,
+        name: N,
+        sig: S,
+    ) -> Result<JValueOwned<'local>>
+    where
+        C: Desc<'local, JClass<'other_local>>,
+        N: AsRef<JNIStr>,
+        S: AsRef<JNIStr>,
+    {
+        // Runtime check that the 'local reference lifetime will be tied to
+        // JNIEnv lifetime for the top JNI stack frame
+        assert_eq!(self.level, JavaVM::thread_attach_guard_level());
+        let sig = sig.as_ref();
+        let field_ty = JavaType::from_str(&sig.to_str())?;
+
+        // go ahead and look up the class since we'll need that for the next call.
+        let class = class.lookup(self)?;
+
+        // SAFETY: We have verified that `class`, `field`, `sig` and `ty` are valid
+        unsafe {
+            self.get_static_field_unchecked(class.as_ref(), (class.as_ref(), name, sig), field_ty)
+        }
+    }
+
+    /// Set a static field. Requires a class lookup and a field id lookup internally.
+    pub fn set_static_field<'other_local, C, N, S>(
+        &mut self,
+        class: C,
+        name: N,
+        sig: S,
+        value: JValue,
+    ) -> Result<()>
+    where
+        C: Desc<'local, JClass<'other_local>>,
+        N: AsRef<JNIStr>,
+        S: AsRef<JNIStr>,
+    {
+        let sig = sig.as_ref();
+        let field_ty = JavaType::from_str(&sig.to_str())?;
+
+        if value.java_type() != field_ty {
+            return Err(Error::WrongJValueType(value.type_name(), "see java field"));
+        }
+
+        let class = class.lookup(self)?;
+
+        // Safety: We have explicitly checked that the field type matches the value type.
+        unsafe {
+            self.set_static_field_unchecked(class.as_ref(), (class.as_ref(), name, sig), value)
+        }
+    }
+
     /// Looks up the field ID for the given field name and takes the monitor
     /// lock on the given object so the field can be updated without racing
     /// with other Java threads
diff --git a/src/wrapper/objects/jvalue.rs b/src/wrapper/objects/jvalue.rs
index e06db1a0..d0f50662 100644
--- a/src/wrapper/objects/jvalue.rs
+++ b/src/wrapper/objects/jvalue.rs
@@ -4,6 +4,7 @@ use std::fmt::Debug;
 
 use log::trace;
 
+use crate::signature::JavaType;
 use crate::{errors::*, objects::JObject, signature::Primitive, sys::*};
 
 #[cfg(doc)]
@@ -222,6 +223,22 @@ impl<'obj_ref> JValue<'obj_ref> {
         }
     }
 
+    /// Get the corresponding Java type.
+    pub fn java_type(&self) -> JavaType {
+        match *self {
+            Self::Object(_) => JavaType::Object,
+            Self::Void => JavaType::Primitive(Primitive::Void),
+            Self::Byte(_) => JavaType::Primitive(Primitive::Byte),
+            Self::Char(_) => JavaType::Primitive(Primitive::Char),
+            Self::Short(_) => JavaType::Primitive(Primitive::Short),
+            Self::Int(_) => JavaType::Primitive(Primitive::Int),
+            Self::Long(_) => JavaType::Primitive(Primitive::Long),
+            Self::Bool(_) => JavaType::Primitive(Primitive::Boolean),
+            Self::Float(_) => JavaType::Primitive(Primitive::Float),
+            Self::Double(_) => JavaType::Primitive(Primitive::Double),
+        }
+    }
+
     /// Get the primitive type for the enum variant. If it's not a primitive
     /// (i.e. an Object), returns None.
     pub fn primitive_type(&self) -> Option<Primitive> {


Test patch:
diff --git a/tests/jni_api.rs b/tests/jni_api.rs
index 3d7437da..7413b48b 100644
--- a/tests/jni_api.rs
+++ b/tests/jni_api.rs
@@ -19,6 +19,8 @@ use jni::{
 mod util;
 use util::{attach_current_thread, unwrap};
 
+use rusty_fork::rusty_fork_test;
+
 static ARRAYLIST_CLASS: &JNIStr = JNIStr::from_cstr(c"java/util/ArrayList");
 static EXCEPTION_CLASS: &JNIStr = JNIStr::from_cstr(c"java/lang/Exception");
 static ARITHMETIC_EXCEPTION_CLASS: &JNIStr = JNIStr::from_cstr(c"java/lang/ArithmeticException");
@@ -160,6 +162,144 @@ pub fn is_not_same_object_null() {
     .unwrap();
 }
 
+#[test]
+pub fn get_public_field() {
+    attach_current_thread(|env| {
+        // Create a new Point(5, 10)
+        let point = unwrap(
+            env.new_object(
+                c"java/awt/Point",
+                c"(II)V",
+                &[JValue::Int(5), JValue::Int(10)],
+            ),
+            env,
+        )
+        .auto();
+
+        // Get the x field value
+        let x_value = env.get_field(&point, c"x", c"I").unwrap().i().unwrap();
+
+        assert_eq!(x_value, 5);
+
+        // Get the y field value
+        let y_value = env.get_field(&point, c"y", c"I").unwrap().i().unwrap();
+
+        assert_eq!(y_value, 10);
+
+        Ok(())
+    })
+    .unwrap();
+}
+
+#[test]
+pub fn get_public_field_by_id() {
+    attach_current_thread(|env| {
+        // Create a new Point(5, 10)
+        let point = unwrap(
+            env.new_object(
+                c"java/awt/Point",
+                c"(II)V",
+                &[JValue::Int(5), JValue::Int(10)],
+            ),
+            env,
+        )
+        .auto();
+
+        // Get the field ID for x field
+        let field_type = c"I";
+        let field_id = env
+            .get_field_id(c"java/awt/Point", c"x", field_type)
+            .unwrap();
+
+        let field_type = ReturnType::Primitive(Primitive::Int);
+        // Safety: we have just looked up the field ID based on the given class and field_type
+        unsafe {
+            let x_value = env
+                .get_field_unchecked(&point, field_id, field_type)
+                .unwrap()
+                .i()
+                .unwrap();
+
+            assert_eq!(x_value, 5);
+        }
+
+        Ok(())
+    })
+    .unwrap();
+}
+
+#[test]
+pub fn set_public_field() {
+    attach_current_thread(|env| {
+        // Create a new Point(5, 10)
+        let point = unwrap(
+            env.new_object(
+                c"java/awt/Point",
+                c"(II)V",
+                &[JValue::Int(5), JValue::Int(10)],
+            ),
+            env,
+        )
+        .auto();
+
+        // Set the x field to a new value
+        env.set_field(&point, c"x", c"I", JValue::Int(15)).unwrap();
+
+        // Verify the field was set
+        let x_value = env.get_field(&point, c"x", c"I").unwrap().i().unwrap();
+
+        assert_eq!(x_value, 15);
+
+        // Set the y field to a new value
+        env.set_field(&point, c"y", c"I", JValue::Int(25)).unwrap();
+
+        // Verify the field was set
+        let y_value = env.get_field(&point, c"y", c"I").unwrap().i().unwrap();
+
+        assert_eq!(y_value, 25);
+
+        Ok(())
+    })
+    .unwrap();
+}
+
+#[test]
+pub fn set_public_field_by_id() {
+    attach_current_thread(|env| {
+        // Create a new Point(5, 10)
+        let point = unwrap(
+            env.new_object(
+                c"java/awt/Point",
+                c"(II)V",
+                &[JValue::Int(5), JValue::Int(10)],
+            ),
+            env,
+        )
+        .auto();
+
+        // Get the field ID for x field
+        let field_type = c"I";
+        let field_id = env
+            .get_field_id(c"java/awt/Point", c"x", field_type)
+            .unwrap();
+
+        // Set the x field using the field ID
+        // Safety: we have just looked up the field ID based on the given field name and type
+        unsafe {
+            env.set_field_unchecked(&point, field_id, JValue::Int(15))
+                .unwrap();
+        }
+
+        // Verify the field was set
+        let x_value = env.get_field(&point, c"x", c"I").unwrap().i().unwrap();
+
+        assert_eq!(x_value, 15);
+
+        Ok(())
+    })
+    .unwrap();
+}
+
 #[test]
 pub fn get_static_public_field() {
     attach_current_thread(|env| {
@@ -187,18 +327,162 @@ pub fn get_static_public_field_by_id() {
             .unwrap();
 
         let field_type = JavaType::Primitive(Primitive::Int);
-        let min_int_value = env
-            .get_static_field_unchecked(INTEGER_CLASS, field_id, field_type)
+        // Safety: we have just looked up the field ID based on the given class and field_type
+        unsafe {
+            let min_int_value = env
+                .get_static_field_unchecked(INTEGER_CLASS, field_id, field_type)
+                .unwrap()
+                .i()
+                .unwrap();
+
+            assert_eq!(min_int_value, i32::MIN);
+        }
+
+        Ok(())
+    })
+    .unwrap();
+}
+
+rusty_fork_test! {
+#[test]
+fn set_static_public_field() {
+    attach_current_thread(|env| {
+        // We'll use System.in which is a mutable static field
+
+        // Get the original System.in value
+        let original_in = env
+            .get_static_field(c"java/lang/System", c"in", c"Ljava/io/InputStream;")
             .unwrap()
-            .i()
+            .l()
             .unwrap();
 
-        assert_eq!(min_int_value, i32::MIN);
+        // Create a new ByteArrayInputStream as a different InputStream
+        let byte_array = env.new_byte_array(10).unwrap();
+        let new_input_stream = env
+            .new_object(
+                c"java/io/ByteArrayInputStream",
+                c"([B)V",
+                &[JValue::from(&byte_array)],
+            )
+            .unwrap();
+
+        // Set System.in to our new ByteArrayInputStream
+        env.set_static_field(
+            c"java/lang/System",
+            c"in",
+            c"Ljava/io/InputStream;",
+            JValue::from(&new_input_stream),
+        )
+        .unwrap();
+
+        // Verify the field was set by getting it again and checking it's no longer null
+        let current_in = env
+            .get_static_field(c"java/lang/System", c"in", c"Ljava/io/InputStream;")
+            .unwrap()
+            .l()
+            .unwrap();
+
+        // The field should not be null after setting
+        assert!(!current_in.is_null());
+
+        // Restore the original System.in
+        env.set_static_field(
+            c"java/lang/System",
+            c"in",
+            c"Ljava/io/InputStream;",
+            JValue::from(&original_in),
+        )
+        .unwrap();
+
+        // Verify restoration worked - the field should still not be null
+        let restored_in = env
+            .get_static_field(c"java/lang/System", c"in", c"Ljava/io/InputStream;")
+            .unwrap()
+            .l()
+            .unwrap();
+
+        assert!(!restored_in.is_null());
 
         Ok(())
     })
     .unwrap();
 }
+}
+
+rusty_fork_test! {
+#[test]
+fn set_static_public_field_by_id() {
+    attach_current_thread(|env| {
+        // Get the original System.in value
+        let original_in = env
+            .get_static_field(c"java/lang/System", c"in", c"Ljava/io/InputStream;")
+            .unwrap()
+            .l()
+            .unwrap();
+
+        // Get the field ID for System.in
+        let field_type = c"Ljava/io/InputStream;";
+        let field_id = env
+            .get_static_field_id(c"java/lang/System", c"in", field_type)
+            .unwrap();
+
+        // Create a new ByteArrayInputStream as a different InputStream
+        let byte_array = env.new_byte_array(10).unwrap();
+        let new_input_stream = env
+            .new_object(
+                c"java/io/ByteArrayInputStream",
+                c"([B)V",
+                &[JValue::from(&byte_array)],
+            )
+            .unwrap();
+
+        // Set System.in to our new ByteArrayInputStream using the field ID
+        // Safety: we have just looked up the field ID based on the given field name and type
+        unsafe {
+            env.set_static_field_unchecked(
+                c"java/lang/System",
+                field_id,
+                JValue::from(&new_input_stream),
+            )
+            .unwrap();
+        }
+
+        // Verify the field was set by getting it again (this ensures the set operation worked)
+        let current_in = env
+            .get_static_field(c"java/lang/System", c"in", c"Ljava/io/InputStream;")
+            .unwrap()
+            .l()
+            .unwrap();
+
+        // Verify that we can successfully retrieve a non-null input stream
+        assert!(!current_in.is_null());
+
+        // Restore the original System.in using the unchecked method
+        // Safety: we have the correct field ID and value type
+        unsafe {
+            env.set_static_field_unchecked(
+                c"java/lang/System",
+                field_id,
+                JValue::from(&original_in),
+            )
+            .unwrap();
+        }
+
+        // Verify restoration worked
+        let restored_in = env
+            .get_static_field(c"java/lang/System", c"in", c"Ljava/io/InputStream;")
+            .unwrap()
+            .l()
+            .unwrap();
+
+        // The restored value should be the same as original
+        assert!(env.is_same_object(&original_in, &restored_in));
+
+        Ok(())
+    })
+    .unwrap();
+}
+}
 
 /*
 #[test]