Skip to content

Commit 372db87

Browse files
committed
fix(kotlin): recover suspend ABI without kotlin.Metadata
R8 often strips @kotlin.Metadata but leaves the DEX SourceFile (.kt), the same attribute JADX uses to recover Kotlin file names. Treat that as Kotlin, then recognize the Continuation-last / Object-return JVM shape so unused continuation parameters become suspend instead of a raw Java Continuation argument. Skip ContinuationImpl create/invoke bridges.
1 parent 47e0028 commit 372db87

2 files changed

Lines changed: 248 additions & 11 deletions

File tree

dexdec/src/analysis/kotlin_backend/kotlin_model/declared_members.rs

Lines changed: 206 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,9 @@ use std::str::FromStr;
1010

1111
use super::metadata_members::{backing_field_references, MetadataCallable};
1212
use crate::frontend::kotlin_metadata::{KotlinMetadata, TypeReference, Visibility};
13-
use crate::frontend::ClassNode;
14-
use crate::ir::{FieldReference, MethodDescriptor, MethodReference};
13+
use crate::frontend::{ClassNode, MethodNode};
14+
use crate::ir::generic_types::{GenericSignatures, JvmTypeSignature, TypeArgument};
15+
use crate::ir::{ArgType, FieldReference, MethodDescriptor, MethodReference};
1516
use crate::language::kotlin::{KotlinDefaultCallContract, KotlinDefaultMask, KotlinIdentifier};
1617
use std::sync::Arc;
1718

@@ -571,6 +572,7 @@ impl KotlinDeclaredMembers {
571572
singletons: &mut std::collections::BTreeSet<crate::ir::ArgType>,
572573
) {
573574
let Some(Ok(metadata)) = KotlinMetadata::of(&class.annotations) else {
575+
self.collect_suspend_abi(class);
574576
return;
575577
};
576578
let declarations = metadata.declarations();
@@ -726,6 +728,32 @@ impl KotlinDeclaredMembers {
726728
self.fields.insert(reference, property.flags.visibility());
727729
}
728730
}
731+
self.collect_suspend_abi(class);
732+
}
733+
734+
fn collect_suspend_abi(&mut self, class: &ClassNode) {
735+
let continuation_impl = class
736+
.methods()
737+
.iter()
738+
.any(|method| method.name() == "invokeSuspend");
739+
for method in class.methods() {
740+
if method.is_constructor() || method.name() == "<clinit>" {
741+
continue;
742+
}
743+
if method.name() == "create" || method.name() == "invokeSuspend" {
744+
continue;
745+
}
746+
if continuation_impl && method.name() == "invoke" {
747+
continue;
748+
}
749+
let Some(declaration) = suspend_abi_declaration(method) else {
750+
continue;
751+
};
752+
let reference = method_reference(class, method);
753+
self.suspend_functions
754+
.entry(reference)
755+
.or_insert(declaration);
756+
}
729757
}
730758

731759
fn index_default_calls(
@@ -931,6 +959,73 @@ impl KotlinDeclaredMembers {
931959
}
932960
}
933961

962+
fn method_reference(class: &ClassNode, method: &MethodNode) -> MethodReference {
963+
MethodReference {
964+
owner: class.class_type().clone(),
965+
name: method.name().to_string(),
966+
descriptor: MethodDescriptor {
967+
parameters: method.param_types().to_vec(),
968+
return_type: method.return_type().clone(),
969+
},
970+
}
971+
}
972+
973+
fn suspend_abi_declaration(method: &MethodNode) -> Option<KotlinSuspendDeclaration> {
974+
if method.return_type() != &ArgType::object("java/lang/Object") {
975+
return None;
976+
}
977+
let continuation_parameter = method.param_types().len().checked_sub(1)?;
978+
if !is_continuation_type(&method.param_types()[continuation_parameter]) {
979+
return None;
980+
}
981+
Some(KotlinSuspendDeclaration {
982+
continuation_parameter,
983+
return_type: suspend_abi_return_type(method),
984+
})
985+
}
986+
987+
fn suspend_abi_return_type(method: &MethodNode) -> ArgType {
988+
continuation_result_type(method).unwrap_or_else(|| ArgType::object("java/lang/Object"))
989+
}
990+
991+
fn continuation_result_type(method: &MethodNode) -> Option<ArgType> {
992+
let signature = method
993+
.signature
994+
.as_deref()
995+
.and_then(|signature| GenericSignatures::method(signature).ok())
996+
.or_else(|| {
997+
method
998+
.override_semantics
999+
.as_ref()
1000+
.and_then(|semantics| semantics.inherited_signature.clone())
1001+
})?;
1002+
let last = signature.parameter_types.last()?;
1003+
continuation_type_argument(last).map(|ty| ty.erased())
1004+
}
1005+
1006+
fn continuation_type_argument(ty: &JvmTypeSignature) -> Option<&JvmTypeSignature> {
1007+
let JvmTypeSignature::ClassType(class) = ty else {
1008+
return None;
1009+
};
1010+
if !is_continuation_class_name(&class.erased_name()) {
1011+
return None;
1012+
}
1013+
match class.type_arguments.first()? {
1014+
TypeArgument::Super(inner) | TypeArgument::Exact(inner) | TypeArgument::Extends(inner) => {
1015+
Some(inner)
1016+
}
1017+
TypeArgument::Unbounded => None,
1018+
}
1019+
}
1020+
1021+
fn is_continuation_type(ty: &ArgType) -> bool {
1022+
ty.as_object().is_some_and(is_continuation_class_name)
1023+
}
1024+
1025+
fn is_continuation_class_name(name: &str) -> bool {
1026+
name == "kotlin/coroutines/Continuation" || name.ends_with("/Continuation")
1027+
}
1028+
9341029
fn property_accessor_reference(
9351030
class: &ClassNode,
9361031
signature: &crate::frontend::kotlin_metadata::JvmSignature,
@@ -941,3 +1036,112 @@ fn property_accessor_reference(
9411036
descriptor: MethodDescriptor::from_str(&signature.descriptor).ok()?,
9421037
})
9431038
}
1039+
1040+
#[cfg(test)]
1041+
mod tests {
1042+
use super::*;
1043+
use crate::frontend::{AccessInfo, ClassInfo, MethodInfo};
1044+
1045+
fn class_with_methods(name: &str, methods: Vec<MethodNode>) -> ClassNode {
1046+
let mut class = ClassNode::new(
1047+
0,
1048+
ClassInfo::from_type_descriptor(&format!("L{name};")).expect("class descriptor"),
1049+
AccessInfo::for_class(0x1),
1050+
);
1051+
for method in methods {
1052+
class.add_method(method);
1053+
}
1054+
class
1055+
}
1056+
1057+
fn method(name: &str, parameters: Vec<ArgType>, return_type: ArgType) -> MethodNode {
1058+
MethodNode::new(
1059+
0,
1060+
MethodInfo::new(
1061+
"Lsample/Host;".to_string(),
1062+
name.to_string(),
1063+
parameters,
1064+
return_type,
1065+
),
1066+
AccessInfo::for_method(0x9),
1067+
)
1068+
}
1069+
1070+
#[test]
1071+
fn continuation_type_accepts_obfuscated_package() {
1072+
assert!(is_continuation_type(&ArgType::object(
1073+
"kotlin/coroutines/Continuation"
1074+
)));
1075+
assert!(is_continuation_type(&ArgType::object("ef1/Continuation")));
1076+
assert!(!is_continuation_type(&ArgType::object("java/lang/Object")));
1077+
assert!(!is_continuation_type(&ArgType::INT));
1078+
}
1079+
1080+
#[test]
1081+
fn suspend_abi_reads_continuation_result_type() {
1082+
let method = method(
1083+
"await",
1084+
vec![
1085+
ArgType::object("java/lang/String"),
1086+
ArgType::object("kotlin/coroutines/Continuation"),
1087+
],
1088+
ArgType::object("java/lang/Object"),
1089+
)
1090+
.with_signature(Some(
1091+
"(Ljava/lang/String;Lkotlin/coroutines/Continuation<-Ljava/lang/Integer;>;)Ljava/lang/Object;"
1092+
.into(),
1093+
));
1094+
let declaration = suspend_abi_declaration(&method).expect("suspend ABI");
1095+
assert_eq!(declaration.continuation_parameter, 1);
1096+
assert_eq!(
1097+
declaration.return_type,
1098+
ArgType::object("java/lang/Integer")
1099+
);
1100+
}
1101+
1102+
#[test]
1103+
fn collect_suspend_abi_skips_continuation_impl_invoke() {
1104+
let class = class_with_methods(
1105+
"sample/StateMachine",
1106+
vec![
1107+
method(
1108+
"invokeSuspend",
1109+
vec![ArgType::object("java/lang/Object")],
1110+
ArgType::object("java/lang/Object"),
1111+
),
1112+
method(
1113+
"invoke",
1114+
vec![
1115+
ArgType::object("java/lang/String"),
1116+
ArgType::object("kotlin/coroutines/Continuation"),
1117+
],
1118+
ArgType::object("java/lang/Object"),
1119+
),
1120+
method(
1121+
"await",
1122+
vec![ArgType::object("kotlin/coroutines/Continuation")],
1123+
ArgType::object("java/lang/Object"),
1124+
),
1125+
],
1126+
);
1127+
let declared = KotlinDeclaredMembers::analyze(&[&class], &|_, _| None);
1128+
let invoke = method_reference(
1129+
&class,
1130+
class
1131+
.methods()
1132+
.iter()
1133+
.find(|method| method.name() == "invoke")
1134+
.expect("invoke"),
1135+
);
1136+
let await_method = method_reference(
1137+
&class,
1138+
class
1139+
.methods()
1140+
.iter()
1141+
.find(|method| method.name() == "await")
1142+
.expect("await"),
1143+
);
1144+
assert!(declared.suspend_declaration(&invoke).is_none());
1145+
assert!(declared.suspend_declaration(&await_method).is_some());
1146+
}
1147+
}

dexdec/src/api/decompiler.rs

Lines changed: 42 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -267,9 +267,10 @@ impl Decompiler {
267267
/// The language a class was written in, as far as the class itself says.
268268
///
269269
/// The Kotlin compiler stamps everything it emits with `@kotlin.Metadata`,
270-
/// down to the synthetic classes it makes for lambdas, and nothing else
271-
/// writes that annotation. A class carrying none was not compiled from
272-
/// Kotlin, so Java is what it should be read back as.
270+
/// down to the synthetic classes it makes for lambdas. R8 often strips that
271+
/// annotation but leaves the DEX `SourceFile` (`.kt`), which is the same
272+
/// attribute JADX uses to recover Kotlin file names. A class with neither
273+
/// signal is read back as Java.
273274
///
274275
/// Only the class declaration is read; no method body is decoded.
275276
pub fn source_language(
@@ -280,12 +281,10 @@ impl Decompiler {
280281
let Some(node) = self.context.load_class_deferred(&class)? else {
281282
return Err(DecompileError::ClassNotFound(class));
282283
};
283-
let kotlin = node.annotations.iter().any(KotlinMetadata::is_metadata);
284-
Ok(if kotlin {
285-
SourceLanguage::Kotlin
286-
} else {
287-
SourceLanguage::Java
288-
})
284+
Ok(inferred_source_language(
285+
&node.annotations,
286+
node.source_file.as_deref(),
287+
))
289288
}
290289

291290
/// Inspect one class declaration without decoding any method body.
@@ -582,6 +581,23 @@ pub fn kotlin_source_path(descriptor: &str) -> PathBuf {
582581
source_path(descriptor, SourceLanguage::Kotlin)
583582
}
584583

584+
fn inferred_source_language(
585+
annotations: &[crate::frontend::AnnotationNode],
586+
source_file: Option<&str>,
587+
) -> SourceLanguage {
588+
if annotations.iter().any(KotlinMetadata::is_metadata) || is_kotlin_source_file(source_file) {
589+
SourceLanguage::Kotlin
590+
} else {
591+
SourceLanguage::Java
592+
}
593+
}
594+
595+
fn is_kotlin_source_file(source_file: Option<&str>) -> bool {
596+
source_file.is_some_and(|name| {
597+
name.ends_with(".kt") || name.ends_with(".kts") || name.ends_with(".ktm")
598+
})
599+
}
600+
585601
#[cfg(test)]
586602
mod tests {
587603
use super::*;
@@ -601,4 +617,21 @@ mod tests {
601617
ClassSelector::Listed(BTreeSet::from(["LA;".to_string(), "LB;".to_string()]))
602618
);
603619
}
620+
621+
#[test]
622+
fn source_file_kt_selects_kotlin_without_metadata() {
623+
assert_eq!(
624+
inferred_source_language(&[], Some("PipHintTracker.kt")),
625+
SourceLanguage::Kotlin
626+
);
627+
assert_eq!(
628+
inferred_source_language(&[], Some("Script.kts")),
629+
SourceLanguage::Kotlin
630+
);
631+
assert_eq!(
632+
inferred_source_language(&[], Some("Main.java")),
633+
SourceLanguage::Java
634+
);
635+
assert_eq!(inferred_source_language(&[], None), SourceLanguage::Java);
636+
}
604637
}

0 commit comments

Comments
 (0)