0
0
mirror of https://github.com/signalapp/libsignal.git synced 2024-09-19 19:42:19 +02:00

Lookup all Java classes through preload-aware fn

Change all usages of JNIEnv::find_class to a helper that checks the preload 
list first. Add the real thing to clippy's disallowed-methods list.
This commit is contained in:
Alex Konradi 2024-04-18 17:29:00 -04:00 committed by GitHub
parent ff10bdc76b
commit dc04e0ac2f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 181 additions and 139 deletions

View File

@ -1 +1,5 @@
too-many-arguments-threshold = 8
disallowed-methods = [
{ path = "jni::JNIEnv::find_class", reason = "use lookup helper instead" },
".." # keep any defaults
]

View File

@ -85,15 +85,20 @@ pub unsafe extern "C" fn Java_org_signal_libsignal_internal_Native_SealedSender_
.try_into()
.expect("too many recipients"),
|env| -> SignalJniResult<_> {
let recipient_class = env.find_class(jni_class_name!(
let recipient_class = find_class(
env,
jni_class_name!(
org.signal
.libsignal
.protocol
.SealedSenderMultiRecipientMessage
::Recipient
))?;
let service_id_class =
env.find_class(jni_class_name!(org.signal.libsignal.protocol.ServiceId))?;
),
)?;
let service_id_class = find_class(
env,
jni_class_name!(org.signal.libsignal.protocol.ServiceId),
)?;
let mut excluded_recipient_java_service_ids = vec![];

View File

@ -0,0 +1,149 @@
//
// Copyright 2024 Signal Messenger, LLC.
// SPDX-License-Identifier: AGPL-3.0-only
//
use std::collections::HashMap;
use jni::objects::{AutoLocal, GlobalRef, JClass, JObject, JThrowable};
use jni::JNIEnv;
use once_cell::sync::OnceCell;
use crate::jni::{BridgeLayerError, ThrownException};
static PRELOADED_CLASSES: OnceCell<HashMap<&'static str, GlobalRef>> = OnceCell::new();
const PRELOADED_CLASS_NAMES: &[&str] = &[
jni_class_name!(org.signal.libsignal.attest.AttestationFailedException),
jni_class_name!(org.signal.libsignal.net.CdsiInvalidTokenException),
jni_class_name!(org.signal.libsignal.net.CdsiLookupResponse),
jni_class_name!(org.signal.libsignal.net.CdsiLookupResponse::Entry),
jni_class_name!(org.signal.libsignal.net.CdsiProtocolException),
jni_class_name!(org.signal.libsignal.net.ChatService),
jni_class_name!(org.signal.libsignal.net.ChatService::DebugInfo),
jni_class_name!(org.signal.libsignal.net.ChatService::Response),
jni_class_name!(org.signal.libsignal.net.ChatService::ResponseAndDebugInfo),
jni_class_name!(org.signal.libsignal.net.ChatServiceException),
jni_class_name!(org.signal.libsignal.net.ChatServiceInactiveException),
jni_class_name!(org.signal.libsignal.net.NetworkException),
jni_class_name!(org.signal.libsignal.net.RetryLaterException),
jni_class_name!(
org.signal
.libsignal
.sgxsession
.SgxCommunicationFailureException
),
jni_class_name!(org.signal.libsignal.svr.DataMissingException),
jni_class_name!(org.signal.libsignal.svr.RestoreFailedException),
jni_class_name!(org.signal.libsignal.svr.SvrException),
#[cfg(feature = "testing-fns")]
jni_class_name!(org.signal.libsignal.internal.TestingException),
];
/// Preloads some classes from the provided [`JNIEnv`].
///
/// Uses [`JNIEnv::find_class`] to cache some classes in a static variable for
/// later. These cached class instances can later be retrieved with
/// [`find_class`].
pub fn preload_classes(env: &mut JNIEnv<'_>) -> Result<(), BridgeLayerError> {
let _saved = PRELOADED_CLASSES.get_or_try_init(|| {
let no_class_found_exceptions = {
let first = real_jni_find_class(env, jni_class_name!(java.lang.NoClassDefFoundError))?;
let second =
real_jni_find_class(env, jni_class_name!(java.lang.ClassNotFoundException))?;
[first, second]
};
let is_not_found_exception =
|throwable: &JThrowable<'_>, env: &JNIEnv<'_>| -> jni::errors::Result<bool> {
for no_class_found in &no_class_found_exceptions {
if env.is_same_object(env.get_object_class(throwable)?, no_class_found)? {
return Ok(true);
}
}
Ok(false)
};
PRELOADED_CLASS_NAMES
.iter()
.map(|name| {
let find_class_result =
real_jni_find_class(env, name).map(|c| AutoLocal::new(c, env));
let throwable = AutoLocal::new(env.exception_occurred()?, env);
let class = if throwable.is_null() {
find_class_result?
} else {
env.exception_clear()?;
if !is_not_found_exception(&throwable, env)? {
return Err(BridgeLayerError::CallbackException(
"FindClass",
ThrownException::new(env, throwable)?,
));
}
// Ignore failures caused by nonexistent classes. This
// allows the same native library to be used with JAR files
// that contain different subsets of classes.
AutoLocal::new(JObject::null().into(), env)
};
let global_ref = env.new_global_ref(class)?;
Ok((*name, global_ref))
})
.collect::<Result<HashMap<_, _>, BridgeLayerError>>()
})?;
Ok(())
}
/// Looks up a class by name.
///
/// Checks the set of preloaded classes first to prevent lookup errors caused by
/// the class path differing for different threads. Use this instead of
/// [`JNIEnv::find_class`].
pub fn find_class<'output>(
env: &mut JNIEnv<'output>,
name: &'static str,
) -> Result<JClass<'output>, jni::errors::Error> {
match get_preloaded_class(env, name)? {
Some(c) => Ok(c),
None => real_jni_find_class(env, name),
}
}
/// Loads a previously cached class.
///
/// Looks up the given class name saved by [`preload_classes`], if there was
/// one. Returns `Ok(None)` otherwise.
///
/// This is useful on platforms like Android where JVM-spawned threads have
/// access to application-defined classes but native threads (including Tokio
/// runtime threads) do not. A class object cached from a JVM-spawned thread can
/// be used by natively-spawned threads to access application-defined types.
fn get_preloaded_class<'output>(
env: &mut JNIEnv<'output>,
name: &'static str,
) -> Result<Option<JClass<'output>>, jni::errors::Error> {
let class = PRELOADED_CLASSES
.get()
.expect("Java classes were not preloaded")
.get(&name);
let local_ref = class.map(|class| env.new_local_ref(class)).transpose()?;
Ok(local_ref.map(Into::into))
}
/// Equivalent to [`JNIEnv::find_class`].
///
/// That function is marked as disallowed because its behavior is different on
/// Android depending on what thread it is called from. In most cases, the
/// [`find_class`] function in this module should be used instead. However,
/// since the real thing is needed to implement that helper, this function
/// exists to provide a narrowly-scoped `#[allow]`ed exception to the rule.
#[allow(clippy::disallowed_methods)]
fn real_jni_find_class<'output>(
env: &mut JNIEnv<'output>,
name: &'static str,
) -> Result<JClass<'output>, jni::errors::Error> {
env.find_class(name)
}

View File

@ -520,7 +520,7 @@ impl<'a> ResultTypeInfo<'a> for crate::cds2::Cds2Metrics {
)?;
let jmap = JMap::from_env(env, &jobj)?;
let long_class = env.find_class(jni_class_name!(java.lang.Long))?;
let long_class = find_class(env, jni_class_name!(java.lang.Long))?;
for (k, v) in self.0 {
let k = k.convert_into(env)?;
let v = new_object(env, &long_class, jni_args!((v => long) -> void))?;
@ -983,9 +983,7 @@ impl<'a> ResultTypeInfo<'a> for libsignal_net::cdsi::LookupResponse {
let entry_class = {
const ENTRY_CLASS: &str =
jni_class_name!(org.signal.libsignal.net.CdsiLookupResponse::Entry);
get_preloaded_class(env, ENTRY_CLASS)
.transpose()
.unwrap_or_else(|| env.find_class(ENTRY_CLASS))?
find_class(env, ENTRY_CLASS)?
};
for entry in records {
@ -1019,9 +1017,7 @@ impl<'a> ResultTypeInfo<'a> for libsignal_net::cdsi::LookupResponse {
let class = {
const RESPONSE_CLASS: &str =
jni_class_name!(org.signal.libsignal.net.CdsiLookupResponse);
get_preloaded_class(env, RESPONSE_CLASS)
.transpose()
.unwrap_or_else(|| env.find_class(RESPONSE_CLASS))?
find_class(env, RESPONSE_CLASS)?
};
Ok(new_object(
env,
@ -1065,9 +1061,7 @@ impl<'a> ResultTypeInfo<'a> for libsignal_net::chat::Response {
let class = {
const RESPONSE_CLASS: &str =
jni_class_name!(org.signal.libsignal.net.ChatService::Response);
get_preloaded_class(env, RESPONSE_CLASS)
.transpose()
.unwrap_or_else(|| env.find_class(RESPONSE_CLASS))?
find_class(env, RESPONSE_CLASS)?
};
Ok(new_object(
@ -1109,9 +1103,7 @@ impl<'a> ResultTypeInfo<'a> for libsignal_net::chat::DebugInfo {
let class = {
const RESPONSE_CLASS: &str =
jni_class_name!(org.signal.libsignal.net.ChatService::DebugInfo);
get_preloaded_class(env, RESPONSE_CLASS)
.transpose()
.unwrap_or_else(|| env.find_class(RESPONSE_CLASS))?
find_class(env, RESPONSE_CLASS)?
};
Ok(new_object(
@ -1142,9 +1134,7 @@ impl<'a> ResultTypeInfo<'a> for ResponseAndDebugInfo {
let class = {
const RESPONSE_CLASS: &str =
jni_class_name!(org.signal.libsignal.net.ChatService::ResponseAndDebugInfo);
get_preloaded_class(env, RESPONSE_CLASS)
.transpose()
.unwrap_or_else(|| env.find_class(RESPONSE_CLASS))?
find_class(env, RESPONSE_CLASS)?
};
Ok(new_object(

View File

@ -2,7 +2,6 @@
// Copyright 2020-2021 Signal Messenger, LLC.
// SPDX-License-Identifier: AGPL-3.0-only
//
use std::collections::HashMap;
use std::error::Error;
use std::fmt::Display;
use std::marker::PhantomData;
@ -10,11 +9,10 @@ use std::marker::PhantomData;
use attest::enclave::Error as EnclaveError;
use attest::hsm_enclave::Error as HsmEnclaveError;
use device_transfer::Error as DeviceTransferError;
use jni::objects::{AutoLocal, GlobalRef, JThrowable, JValue, JValueOwned};
use jni::objects::{GlobalRef, JThrowable, JValue, JValueOwned};
use jni::JavaVM;
use libsignal_net::svr3::Error as Svr3Error;
use libsignal_protocol::*;
use once_cell::sync::OnceCell;
use signal_crypto::Error as SignalCryptoError;
use signal_pin::Error as PinError;
use usernames::{UsernameError, UsernameLinkError};
@ -31,6 +29,9 @@ pub(crate) use jni::JNIEnv;
mod args;
pub use args::*;
mod class_lookup;
pub use class_lookup::*;
#[macro_use]
mod convert;
pub use convert::*;
@ -617,9 +618,7 @@ where
let throwable = env
.new_string(error.to_string())
.and_then(|message| {
let class = get_preloaded_class(env, exception_type)
.transpose()
.unwrap_or_else(|| env.find_class(exception_type))?;
let class = find_class(env, exception_type)?;
Ok(new_object(env, class, jni_args!((message => java.lang.String) -> void))?.into())
})
.map_err(Into::into);
@ -750,112 +749,6 @@ fn check_exceptions_and_convert_result<'output, R: TryFrom<JValueOwned<'output>>
}
}
static PRELOADED_CLASSES: OnceCell<HashMap<&'static str, GlobalRef>> = OnceCell::new();
const PRELOADED_CLASS_NAMES: &[&str] = &[
jni_class_name!(org.signal.libsignal.attest.AttestationFailedException),
jni_class_name!(org.signal.libsignal.net.CdsiInvalidTokenException),
jni_class_name!(org.signal.libsignal.net.CdsiLookupResponse),
jni_class_name!(org.signal.libsignal.net.CdsiLookupResponse::Entry),
jni_class_name!(org.signal.libsignal.net.CdsiProtocolException),
jni_class_name!(org.signal.libsignal.net.ChatService),
jni_class_name!(org.signal.libsignal.net.ChatService::DebugInfo),
jni_class_name!(org.signal.libsignal.net.ChatService::Response),
jni_class_name!(org.signal.libsignal.net.ChatService::ResponseAndDebugInfo),
jni_class_name!(org.signal.libsignal.net.ChatServiceException),
jni_class_name!(org.signal.libsignal.net.ChatServiceInactiveException),
jni_class_name!(org.signal.libsignal.net.NetworkException),
jni_class_name!(org.signal.libsignal.net.RetryLaterException),
jni_class_name!(
org.signal
.libsignal
.sgxsession
.SgxCommunicationFailureException
),
jni_class_name!(org.signal.libsignal.svr.DataMissingException),
jni_class_name!(org.signal.libsignal.svr.RestoreFailedException),
jni_class_name!(org.signal.libsignal.svr.SvrException),
#[cfg(feature = "testing-fns")]
jni_class_name!(org.signal.libsignal.internal.TestingException),
];
/// Preloads some classes from the provided [`JNIEnv`].
///
/// Uses [`JNIEnv::find_class`] to cache some classes in a static variable for
/// later. These cached class instances can later be retrieved with
/// [`get_preloaded_class`].
pub fn preload_classes(env: &mut JNIEnv<'_>) -> Result<(), BridgeLayerError> {
let _saved = PRELOADED_CLASSES.get_or_try_init(|| {
let no_class_found_exceptions = {
let first = env.find_class(jni_class_name!(java.lang.NoClassDefFoundError))?;
let second = env.find_class(jni_class_name!(java.lang.ClassNotFoundException))?;
[first, second]
};
let is_not_found_exception =
|throwable: &JThrowable<'_>, env: &JNIEnv<'_>| -> jni::errors::Result<bool> {
for no_class_found in &no_class_found_exceptions {
if env.is_same_object(env.get_object_class(throwable)?, no_class_found)? {
return Ok(true);
}
}
Ok(false)
};
PRELOADED_CLASS_NAMES
.iter()
.map(|name| {
let find_class_result = env.find_class(name).map(|c| AutoLocal::new(c, env));
let throwable = AutoLocal::new(env.exception_occurred()?, env);
let class = if throwable.is_null() {
find_class_result?
} else {
env.exception_clear()?;
if !is_not_found_exception(&throwable, env)? {
return Err(BridgeLayerError::CallbackException(
"FindClass",
ThrownException::new(env, throwable)?,
));
}
// Ignore failures caused by nonexistent classes. This
// allows the same native library to be used with JAR files
// that contain different subsets of classes.
AutoLocal::new(JObject::null().into(), env)
};
let global_ref = env.new_global_ref(class)?;
Ok((*name, global_ref))
})
.collect::<Result<HashMap<_, _>, BridgeLayerError>>()
})?;
Ok(())
}
/// Loads a previously cached class.
///
/// Looks up the given class name saved by [`preload_classes`], if there was
/// one. Returns `Ok(None)` otherwise.
///
/// This is useful on platforms like Android where JVM-spawned threads have
/// access to application-defined classes but native threads (including Tokio
/// runtime threads) do not. A class object cached from a JVM-spawned thread can
/// be used by natively-spawned threads to access application-defined types.
pub fn get_preloaded_class<'output>(
env: &mut JNIEnv<'output>,
name: &'static str,
) -> Result<Option<JClass<'output>>, jni::errors::Error> {
let class = PRELOADED_CLASSES
.get()
.expect("Java classes were not preloaded")
.get(&name);
let local_ref = class.map(|class| env.new_local_ref(class)).transpose()?;
Ok(local_ref.map(Into::into))
}
/// Constructs a new object using [`JniArgs`].
///
/// Wraps [`JNIEnv::new_object`]; all arguments are the same.
@ -911,7 +804,7 @@ pub fn check_jobject_type(
return Err(BridgeLayerError::NullPointer(Some(class_name)));
}
let class = env.find_class(class_name)?;
let class = find_class(env, class_name)?;
if !env.is_instance_of(obj, class)? {
return Err(BridgeLayerError::BadJniParameter(class_name));

View File

@ -110,7 +110,8 @@ impl<'a> JniIdentityKeyStore<'a> {
key_handle,
)?;
let direction_class = env.find_class(
let direction_class = find_class(
env,
jni_class_name!(org.signal.libsignal.protocol.state.IdentityKeyStore::Direction),
)?;
let field_name = match direction {