Lost in Migration: Exposing Android Framework Vulnerabilities in Parallel Java-Kotlin Implementations Rui Li
Singapore Management University Singapore [email protected]
Wenrui Diao
Shandong University Qingdao, China [email protected]
arXiv:2606.07420v1 [cs.CR] 5 Jun 2026
Abstract Android has adopted Kotlin alongside Java across apps and core system components. During this shift, we observe parallel implementations in the Android Open Source Project (AOSP) where the same component is implemented in both Java and Kotlin. In principle, their functional purposes are identical. In practice, subtle semantic divergences can appear. Such divergences are not vulnerabilities by themselves, but they provide useful clues that may reveal flaws in surrounding enforcement logic. To the best of our knowledge, this paper presents the first systematic study of Java-Kotlin parallel implementations in the Android framework and examines their security implications. We design and build ParaDroid, an analysis framework that identifies parallel methods at scale and compares their behaviors. ParaDroid normalizes code into a bytecode-level intermediate representation, reconstructs class-to-source mappings, and uses large language models to reason about method semantics and identify behavioral divergences. Evaluated on AOSP Android 14-16, ParaDroid identified 329 parallel method pairs and 37 vulnerable divergences. We responsibly disclosed the exploitable issues to the Android Security Team. Three vulnerabilities and two bugs have been confirmed, and two CVE IDs have been assigned. Our results demonstrate that parallel Java-Kotlin code paths provide a practical surface for discovering security flaws in modern Android.
CCS Concepts • Security and privacy → Software and application security.
Keywords Android OS, Language Migration, Vulnerability Discovery
1
Introduction
Android powers billions of devices and remains the most widely deployed mobile operating system. As the platform evolves, Google has promoted Kotlin to first-class status for Android development [2]. Kotlin compiles to JVM bytecode and interoperates with existing Java code [8], which has encouraged its adoption across both applications and system components. The Android Open Source Project (AOSP) reflects this trend, with a growing share of Kotlin code emerging alongside the still-dominant Java codebase. Prior work [31] and official guidance [8] highlight Kotlin’s advantages over Java, including concise syntax, null-safety, higher-order functions, and coroutines. These features reduce boilerplate and improve maintainability, while integrating seamlessly with Java. While existing studies primarily discuss the attractiveness of Kotlin, they fail to investigate how coexisting Java and Kotlin implementations of the same subsystem may evolve differently or pose security concerns.
Debin Gao
Singapore Management University Singapore [email protected]
In recent versions of AOSP, we observe a recurring phenomenon that we term parallel implementations: two codepaths, one in Java and one in Kotlin, that are designed to serve an identical functional purpose. Specifically, they aim to achieve the same operational outcome, even though their internal implementation logic and coding patterns may differ. These pairs may reside in the same or different packages, use similar class names, or be activated at runtime through version checks. Parallel implementations often appear during staged migrations or refactoring. Conceptually, they are meant to be interchangeable. In practice, however, subtle semantic differences can emerge. While such differences are not always vulnerabilities in isolation, they can provide critical clues that reveal flaws in the surrounding enforcement logic. Our motivating case (Section 3.1) in the Android permission subsystem demonstrates that the divergence between the Java and Kotlin paths exposes an inconsistency in enforcement logic, which could be exploited to obtain unauthorized sensitive information. Our Work. To the best of our knowledge, this is the first systematic analysis specifically targeting the security implications of the co-evolutionary phase in Android’s Java-to-Kotlin migration. We design and build ParaDroid, an analysis framework that identifies parallel methods at scale and compares their behaviors. ParaDroid normalizes Java and Kotlin into a common bytecode-level representation to reconstruct class-to-source mappings and detect candidate parallel pairs. It then standardizes the source code of these targeted pairs into language-agnostic Unified Execution Graphs (UEGs). Finally, it leverages Large Language Models (LLMs) to reason about code-level semantics across the UEGs, identifying behavioral divergences and evaluating potential security risks. We evaluated ParaDroid on AOSP versions 14, 15, and 16. The tool identified 329 parallel method pairs. Within this set, it detected 372 behavioral divergences and flagged 37 cases as potentially security-relevant. Following deduplication and manual review, we identified 11 unique, exploitable vulnerabilities and reported them to the Android Security Team. At the time of submission, Google confirmed five distinct issues. They classified three findings as security vulnerabilities. Two of these vulnerabilities received high-severity ratings and were assigned CVE IDs. One vulnerability received a low-severity rating. Google confirmed the two other issues as functional defects. Contributions. The main contributions of this work are: • Parallel implementation phenomenon. This work exposes parallel Java-Kotlin implementations in AOSP as a systematic source of semantic divergences. These discrepancies serve as critical indicators for uncovering security flaws in surrounding enforcement logic.
Rui Li, Wenrui Diao, and Debin Gao
Roadmap. Section 2 provides background on Kotlin for Android and the threat model. Section 3 presents a motivating case and outlines automated analysis challenges. Section 4 details the design and implementation of ParaDroid. Section 5 presents the evaluation results. Section 6 analyzes discovered vulnerabilities through case studies. Section 7 covers limitations and mitigation strategies. Sections 8 and 9 review related work and conclude the paper.
2
Background and Threat Model
This section provides the necessary background on the Kotlin language and its role in the Android ecosystem. We also introduce the threat model assumed in this paper.
2.1
Kotlin for Android
Kotlin [8] is a modern statically typed programming language introduced by JetBrains in 2011. It compiles to Java bytecode and interoperates seamlessly with Java. In 2017, Google announced official support for Kotlin in Android, and in 2019, launched the “Kotlin First” strategy [2] to promote its adoption in both Android apps and system development. Compared to Java, Kotlin introduces features designed to improve code quality and developer experience. Its concise syntax reduces boilerplate, such as verbose getters, setters, and explicit type declarations [25]. Kotlin also enforces strict null-safety within its type system, helping prevent common errors like null pointer exceptions [2]. Additionally, Kotlin supports lambda expressions and extension functions, facilitating functional programming patterns [7]. It further provides coroutines for structured concurrency, simplifying asynchronous programming by abstracting thread management. Crucially, Kotlin remains fully interoperable with Java, allowing classes from both languages to coexist and interact within the same project. While these features enhance productivity, they also introduce new language semantics and security mechanisms at the system level. According to Google, Kotlin is now “used by over 60% of professional Android developers” [8]. Beyond third-party applications, Kotlin has seen increasing adoption within the Android OS itself. The Android Open Source Project (AOSP) team has been gradually migrating system apps and components from Java to Kotlin [22]. Google’s official contribution guidelines now explicitly state that parts of the AOSP codebase are written in Kotlin and accept submissions in this language [13]. To quantify the accelerating adoption of Kotlin within the OS framework, we conducted a longitudinal tracking of codebase compositions across four recent major releases: Android 13 through 16 (specifically build tags 13.0.0_r70, 14.0.0_r37, 15.0.0_r34, and 16.0.0_r3). As illustrated in Figure 1, while Java remains the dominant language, the volume of Kotlin files has surged from 6,151
100% 98%
File Distribution (%)
• Analysis framework. We designed and implemented ParaDroid to automatically locate parallel method pairs. It compares their behaviors using bytecode normalization, class-source recovery, a language-agnostic unified execution graph, and LLM-assisted semantic reasoning. • Real-world vulnerabilities. This work uncovered security-relevant divergences in core framework components. Attackers can exploit these flaws to achieve privilege escalation and data leakage. Google acknowledged these findings and assigned CVE IDs.
96%
6,151 12,507
94%
20,407
23,654 Kotlin Files
92% 90%
88%
Java Files
109,132 129,481
198,940
Android 14
Android 15
86%
203,219
84% Android 13
Android 16
Figure 1: Trend of Kotlin and Java file counts in AOSP. to 23,654, with the proportion nearly doubling from 5.3% to 10.4%. This trajectory underscores the deepening integration of Kotlin into core AOSP components.
2.2
Threat Model
We consider an adversary who can execute code on a victim Android device through some local or remote means, for example, by installing a malicious app via official markets or third-party channels. The adversary aims to circumvent Android’s security mechanisms, including permission enforcement, sandboxing, and other access control checks, to cause unauthorized behaviors such as privilege escalation or sensitive data leakage. In this work, we specifically focus on vulnerabilities exposed by the clues derived from behavioral discrepancies between the Kotlin and Java implementations of identical AOSP components. It is important to note that this threat model specifies the attacker’s capability and the security impact, rather than guiding the design of ParaDroid. While ParaDroid identifies potentially risky divergences, the threat model serves as the basis for determining their actual exploitability.
3
Motivating Case and Challenges
This section presents a motivating case showing how discrepancies in parallel implementations can be exploited, followed by challenges in systematic analysis.
3.1
Motivating Case
To motivate our study, we examine a concrete instance from the Android framework where parallel implementations of the same functionality lead to security-relevant inconsistencies. In Android 14, we observed that certain system components are implemented in both Java and Kotlin. While there is no official documentation explaining this redundancy, our analysis indicates it is likely a byproduct of a staged refactoring from Java to Kotlin. This is supported by source code comments, such as: “/** Modern implementation of [PermissionManagerServiceInterface]. */” [11]. A representative example is the permission management service, which features two parallel implementations: the Java-based PermissionManagerServiceImpl.java [10] and the Kotlin-based PermissionService.kt [11]. These classes expose overlapping entry points, with the active path selected at boot time based on the Android version (controlled by SdkLevel.isAtLeastV(), checking if the device is running on Android 15 or newer). Devices running Android 15 or newer utilize the Kotlin implementation, while Android 14 retains the Java version. Despite being conceptually equivalent, these two paths differ significantly in enforcement logic.
Lost in Migration
Semantic Divergence. We identified a semantic divergence in the getAppOpPermissionPackages method. This method returns packages that request a specific app-op permission, where app-ops (App Operations) provide finer-grained access control than the traditional permission model. In the Java implementation (Listing 1), the method acts defensively. It verifies whether the specified permission is present in the app-op registry. If the permission does not map to a valid app-op, the internal registry returns null, causing the method to strictly return an empty array. This safeguards the API from leaking data regarding unrelated permission types. 1 public String [] getAppOpPermissionPackages ( @ NonNull String permissionName ) { 2 Objects . requireNonNull ( permissionName , " permissionName " ) ; 3 return PermissionManagerServiceImpl . this . getAppOpPermissionPackagesInternal ( permissionName ) ; 4 } 5 6 private String [] getAppOpPermissionPackagesInternal ( @ NonNull String permName ) { 7 synchronized ( mLock ) { 8 final ArraySet < String > packageNames = mRegistry . getAppOpPermissionPackages ( permName ) ; 9 10 if ( packageNames == null ) { 11 return EmptyArray . STRING ; 12 } 13 return packageNames . toArray ( new String [0]) ; 14 } 15 }
Listing 1: Java version of getAppOpPermissionPackages. In contrast, the Kotlin implementation (Listing 2) attempts to replicate this logic but introduces a control-flow flaw. The conditional check identifies non-app-op permissions but fails to halt execution (missing return, Lines 7-9), allowing the control flow to fall through. Consequently, it returns the package list requesting any permission type, not just app-ops. 1 override fun getAppOpPermissionPackages ( permissionName : String ): Array < String > { 2 ... 3 val permission = service . getState { 4 with ( policy ) { getPermissions () [ permissionName ] } 5 } 6 7 if ( permission == null || ! permission . isAppOp ) { 8 packageNames . toTypedArray () 9 } 10 ... 11 return packageNames . toTypedArray () 12 }
Listing 2: Kotlin version of getAppOpPermissionPackages. Vulnerability and Impact. This behavioral difference causes unauthorized information disclosure. The Java version correctly hides usage data for non-app-op permissions (e.g., RECEIVE_SMS). Conversely, the Kotlin version permits callers to query any permission and enumerate all apps holding it. We confirmed this control-flow flaw by invoking the Kotlin implementation in Android 15 and 16. Although this method is not directly accessible via the public Android SDK, it can be triggered via the Android Debug Bridge (adb) using a service call. We
supplied a non-app-op system permission name, bypassed the intended visibility restriction, and enumerated apps successfully. We reported this issue to the Android Security Team. It was acknowledged and classified as Low Severity (requiring local shell access), nevertheless, it provides concrete evidence that semantic gaps with security risk exist between Java-Kotlin parallel implementations. Summary. This motivating case highlights a critical observation: Java-Kotlin parallel implementations in AOSP Android are rarely straightforward translations. In practice, system developers often introduce subtle semantic deviations for reasons such as logic optimization, bug fixes, or the adoption of new language features. These differences can serve as valuable clues that may reveal underlying security flaws in the complex and extensive Android system. However, identifying these discrepancies is non-trivial. A robust analysis must extend beyond individual methods to examine entire call chains and their interactions. Furthermore, the significant divergence in syntax and coding idioms between Java and Kotlin makes manual comparison prohibitively expensive and error-prone. Consequently, an automated approach is essential. Motivated by these findings, this work conducts a systematic study using ParaDroid to identify and analyze these parallel implementations at scale.
3.2
Challenges for Automated Analysis
To systematically assess the security implications of parallel implementations in AOSP, our goal is to (1) automatically identify all parallel pairs of Java and Kotlin implementations, and (2) detect potentially risky behavioral discrepancies between them. Achieving this requires addressing two key challenges. Challenge 1: Identifying Parallel Implementations. Locating parallel implementations is non-trivial because they could reside in different packages, employ distinct naming conventions, or evolve independently. Furthermore, the syntactic gap between Java and Kotlin, ranging from variable declarations to inheritance models, prevents direct source-level comparison. To our knowledge, there is no practical tool that can convert between Java and Kotlin source code while strictly preserving semantic equivalence. Additionally, differences in language rules, such as method overriding, return type covariance, and nullability constraints, further complicate the mapping process. Consequently, identifying parallel implementations within the massive AOSP codebase poses a significant scalability and accuracy challenge. Solution to Challenge 1. We address this challenge through a three-step process involving bytecode normalization, context recovery, and signature-based identification. We explicitly avoid fuzzy name matching because AOSP contains widespread method name duplication across unrelated classes. Relying on loose matching would introduce excessive false positives and incur prohibitive manual and computational costs. Step 1: Normalizing method declarations. Both Java and Kotlin in AOSP compile into Dalvik bytecode. Extracting DEX files from the compiled AOSP yields a uniform bytecode-level IR. This abstraction neutralizes language-specific syntactic divergences and allows for a unified analysis of the entire framework. Step 2: Reconstructing class-to-source mappings. Compilation obscures the direct link between classes and source files. However,
Rui Li, Wenrui Diao, and Debin Gao
userdebug and eng builds retain crucial debugging metadata. Parsing this metadata from DEX files accurately reconstructs mappings between compiled classes and their original source files. This reconstruction enables tracing bytecode back to its definition. Step 3: Identification via migration patterns. After establishing classsource associations, we compare method signatures to locate parallel candidates. A signature comprises the method name, parameter types, and return type. Because new implementations must preserve original API contracts for backward compatibility, classes with substantial signature overlap qualify as candidates. Manually inspecting file paths, commit logs, and documentation reveals two primary parallel implementation types (Statically Observable and Runtime-Gated), as detailed in Section 4.2. ParaDroid uses these patterns to identify parallel pairs with high precision. Challenge 2: Detecting Security-Critical Discrepancies. Parallel implementations aim for functional equivalence, so their highlevel semantics are generally aligned. However, subtle code-level deviations can introduce inconsistent behaviors that pose security risks. Detecting such risky discrepancies is difficult because it requires determining: (1) where the execution logic diverges, and (2) whether that divergence compromises the security posture. Traditional program analysis techniques, such as AST or CFG comparison, struggle in this context because Java and Kotlin implementations often diverge significantly in structure and coding idioms. For example, Kotlin’s use of extension functions or nullsafety sugar creates syntactic disparities. These disparities create a wide structural gap that renders standard isomorphism checks ineffective, often leading to excessive false positives where code looks different but acts the same. Solution to Challenge 2. To address this challenge, ParaDroid replaces direct raw-code and rigid AST comparisons with a structured hybrid approach. First, we introduce a language-agnostic shallow parser to standardize heterogeneous Java and Kotlin methods into a Unified Execution Graph (UEG). This representation preserves core execution and data-flow logic while removing language-specific syntactic noise. Building upon the UEG, we leverage LLMs to perform crossgraph differential analysis. The LLM performs semantic N:M mapping to dynamically align execution paths. It explicitly pinpoints logical deviations, such as bypassed guards or fail-open fall-throughs, and extracts them into a Structured Divergence Record (SDR). Finally, ParaDroid employs a Contextual Verification and RetrievalAugmented Generation (RAG) mechanism grounded in official Android severity criteria. An ensemble majority-voting strategy complements this process for assessing security risks and suppressing hallucinations. This defense-in-depth pipeline ensures the system reports only genuine, exploitable vulnerabilities.
4
Design of ParaDroid
Based on the solutions to challenges outlined in Section 3, we design and implement ParaDroid, an automated framework for systematically analyzing parallel Java-Kotlin implementations in the Android system. ParaDroid operates by identifying parallel components at scale and performing differential analysis to uncover security-relevant discrepancies. As illustrated in Figure 2, the workflow consists of three distinct phases:
Phase 1: Data Preparation and Normalization. This phase compiles the Android codebase and normalizes heterogeneous source code into a unified intermediate representation to facilitate cross-language analysis. Phase 2: Parallel Method Identification. This phase identifies candidate class pairs and extracts parallel method information across the Java-Kotlin boundary. Phase 3: Security-Relevant Divergence Detection. This phase converts relevant Java and Kotlin code into a unified structured representation. The system then leverages LLM-based reasoning to detect semantic divergences between parallel methods and assess their potential to introduce security vulnerabilities.
4.1
Data Preparation and Normalization
To ensure accurate downstream analysis, ParaDroid first transforms the Android source tree into a normalized state that supports reliable class-to-source mapping. This step is foundational, as it bridges the gap between compiled artifacts and source code, enabling a precise distinction between Java and Kotlin components. Constructing Android OS Image. The analysis begins with the Android Open Source Project (AOSP). To obtain the ground truth of the system’s execution logic, ParaDroid compiles the full AOSP source tree. This produces a system image containing the authentic compiled classes (DEX files) used by the Android framework, ensuring our analysis reflects the actual runtime behavior. Extracting Class and Method Declarations. To enable crosslanguage analysis, ParaDroid abstracts away language-specific syntax by lifting both Java and Kotlin into a common format. It extracts DEX files from the compiled system image and parses the bytecode to reconstruct class and method declarations. These declarations are then normalized into a unified bytecode-level Intermediate Representation (IR). Note that while this low-level IR is crucial for precise signature matching in Phase 2, it lacks the high-level semantic context (e.g., original variable names, inline comments) necessary for LLM reasoning in Phase 3. Reconstructing Class-to-Source Mappings. Compilation typically severs the direct link between binary classes and their source files, which hinders the identification of parallel implementations. To resolve this, ParaDroid leverages the debugging metadata preserved in userdebug or eng builds. By parsing the class descriptor and source file attributes within the DEX files, ParaDroid accurately reconstructs the mapping between every compiled class and its original source definition. It then traverses the AOSP source tree to verify these associations, building a comprehensive index that links bytecode to specific Java or Kotlin files.
4.2
Parallel Methods Identification
Following normalization, ParaDroid proceeds to identify parallel methods – pairs of Java and Kotlin methods that implement identical functionality within the AOSP framework. We categorize these implementations into two distinct types based on their structural and runtime characteristics. Type I: Statically Observable Migrations. This category encompasses implementations that can be identified through file naming
Lost in Migration
Constructing Android OS Image AOSP Code
Extracting Class and Method Declarations
Reconstructing Classto-Source Mappings
Bytecode IR (Dalvik)
Android Security Severity Criteria
Extracting Parallel Methods
Parallel Methods
UEGs
LLM
Behavioral Divergence Detection
Class–Source Associations
Phase 1: Data Preparation and Normalization
Detection of Type I Detection of Type II
Unified Execution Graph (UEG) Construction
Security Assessment & Threat Modeling
Phase 3: Security-Relevant Divergence Detection Security Reports
Phase 2: Parallel Methods Identification
Figure 2: Overview of ParaDroid. conventions and directory structures. These patterns typically reflect a staged migration strategy where the relationship between the legacy Java code and the new Kotlin code is explicitly encoded in the file system. We observe two common manifestations: (1) Co-located Naming Variations: The Java and Kotlin files reside in the same directory but are distinguished by a modified filename convention. AOSP frequently appends a numeric suffix to the Kotlin file (e.g., WifiDialog2.kt), but this pattern also covers other systematic renaming strategies used to avoid class name conflicts. This typically indicates a staged replacement. As shown in Listing 3, WifiDialog.java [15] and WifiDialog2.kt [14] coexist, and the Java documentation explicitly advises migration: “this object will be removed in the near future, please develop in @link WifiDialog2”. This confirms that they are parallel implementations of the Wi-Fi dialog UI. 1 ------------------------ Java - - - - - - - - - - - - - - - - - - - - - - - - - 2 / packages / apps / Settings / src / com / android / settings / wifi / WifiDialog . java 3 /* * 4 * Dialog for users to edit a Wi - Fi network 5 * 6 * Migrating from Wi - Fi SettingsLib to WifiTrackerLib , this object will be removed in the near future , please develop in { @ link WifiDialog2 }. 7 */ 8 public class WifiDialog {...} 9 10 ----------------------- Kotlin - - - - - - - - - - - - - - - - - - - - - - - - 11 / packages / apps / Settings / src / com / android / settings / wifi / WifiDialog2 . kt 12 /* * 13 * Dialog for users to edit a Wi - Fi network 14 */ 15 @ OpenForTesting 16 open class WifiDialog2 {...}
Listing 3: Example of Type I (Co-located Suffix). (2) Directory Shadowing: Java and Kotlin files share an identical filename but reside in parallel directory structures, distinguished by a Kotlin-specific segment in the Kotlin path, such as /kotlin/. This effectively creates a “shadow” implementation. As illustrated in Listing 4, both AudioPreview.java [4] and AudioPreview.kt [5] provide the audio preview dialog. AOSP commit logs confirm this parallel existence and explicitly note the introduction of the Kotlin
version alongside the original Java code (e.g.,“Add Kotlin version of Music Java code” [6]). 1 ------------------------ Java - - - - - - - - - - - - - - - - ---------2 / packages / apps / Music / src / com / android / music / AudioPreview . java 3 /* * 4 * Dialog that comes up in response to various music related VIEW intents . 5 */ 6 public class AudioPreview {...} 7 8 ----------------------- Kotlin - - - - - - - - - - - - - - - ---------9 / packages / apps / Music / kotlin / src / com / android / music / AudioPreview . kt 10 /* * 11 * Dialog that comes up in response to various music related VIEW intents . 12 */ 13 class AudioPreview {...}
Listing 4: Example of Type I (Directory Shadowing). Type II: Runtime-Gated Migrations. The second category is more subtle and cannot be detected via file metadata alone. Implementations may reside in disparate packages with no obvious naming correlation. The system dynamically selects the active implementation at runtime based on the OS version or feature flags. Listing 5 illustrates this within the permission subsystem mentioned in Section 3.1, which co-hosts both the Java-based PermissionManagerServiceImpl and the Kotlin-based PermissionService. A version check (e.g., SdkLevel.isAtLeastV()) governs the control flow: newer Android versions retrieve the Kotlin implementation via the system service registry, while older versions fall back to instantiating the Java class directly. 1 / frameworks / base / core / java / android / permission / PermissionManager . java 2 /* * 3 * Whether to use the new { @ link com . android . server . permission . access . AccessCheckingService }. 4 */ 5 public static final boolean USE_ACCESS_CHECKING_SERVICE = SdkLevel . isAtLeastV () ;
Listing 5: Example of Type II (Runtime Switch).
Rui Li, Wenrui Diao, and Debin Gao
Detection of Type I (Static Patterns). ParaDroid identifies Statically Observable Migrations using a two-step heuristic approach: (1) Path and Filename Matching: ParaDroid traverses the AOSP source tree to index all Java and Kotlin files. It generates candidate pairs based on naming conventions: (a) co-located files sharing a base name but distinguished by systematic variation (e.g., a numeric suffix), or (b) files with identical names where one resides within a Kotlin-specific subdirectory. (2) Method Signature Overlap Check: ParaDroid validates each candidate pair using the class-to-source mappings established in Phase 1. It retrieves the compiled definitions for the corresponding Java and Kotlin classes and compares their method signatures. ParaDroid classifies the pair as a parallel implementation if they share at least one method signature. Although the naming conventions in Type I are strong indicators, this content-based check is necessary. It serves as a safeguard to filter out false positives in which files share a name but perform unrelated tasks. Handling Build Conflicts for Type I (Directory Shadowing): A specific challenge arises with Type I (Directory Shadowing) because the Java and Kotlin files define identical class names. Consequently, they cannot coexist in a single build. Standard AOSP build configurations typically include only one version at a time. To analyze both, ParaDroid performs a separate compilation step. We adjust the build configuration to select the other implementation as the active one. This ensures that the previously excluded file is compiled and available for analysis. We execute this alternate compilation once after the initial file scanning. Detection of Type II (Runtime Patterns). Detecting RuntimeGated Migrations requires analyzing control flow logic. ParaDroid employs targeted data-flow analysis: (1) Version-Gated Branch Tracing: ParaDroid first scans the AOSP codebase to identify OS version check operations. These include API calls matching the pattern SdkLevel.isAtLeast* and comparisons involving Build.VERSION.SDK_INT. For each identified check, the tool performs data-flow analysis to track how the boolean result propagates through assignments, method calls, fields, and return values. When this result determines the path of an if/else statement, ParaDroid inspects both branches. It records the specific class that is instantiated or retrieved within each branch. (2) Matching Java/Kotlin Pairs: ParaDroid uses the class-to-source associations to determine the implementation language of the classes found in the previous step. It identifies instances where one branch loads a Kotlin class and the other loads a Java class. If these two classes share overlapping method signatures, the tool marks them as a parallel pair under Type II. By applying these techniques, ParaDroid identifies the final set of parallel class pairs. Extracting Parallel Methods. For each identified Java-Kotlin class pair, ParaDroid extracts the methods that share identical signatures. We refer to these as parallel methods. They form the basis for our subsequent behavioral comparison.
4.3
Security-Relevant Divergence Detection
The final phase analyzes the parallel methods to identify behavioral discrepancies that pose security risks. It subsequently generates a
comprehensive report. To bridge the semantic gap between Java and Kotlin while maintaining analytical rigor, ParaDroid avoids direct comparisons of raw source code. Instead, the system standardizes the heterogeneous inputs into a unified structured representation and employs a multi-stage, structured LLM reasoning pipeline. Code Context Extraction and UEG Construction. ParaDroid first isolates the code context required for analysis. For each parallel method, it parses the complete call chain and all invoked sub-routines. Using the class-to-source mappings from Phase 1, ParaDroid retrieves the source code for the entry method and its dependencies for downstream processing. The Phase 1 bytecode IR effectively matches signatures but discards critical developer intent (e.g., inline comments, variable naming). It remains too verbose for LLMs to analyze high-level logic. Conversely, supplying raw source text introduces excessive crosslanguage syntactic noise. This noise inevitably undermines security analysis precision. Furthermore, traditional static analyzers (e.g., Soot or WALA) relying on Abstract Syntax Trees (AST) frequently fail or crash on isolated, partially unresolvable framework snippets. To resolve these issues, we introduce a language-agnostic shallow parser that bypasses the full AST generation step. It processes the extracted Java and Kotlin source streams uniformly to transform heterogeneous code pairs into a Unified Execution Graph (UEG). This standardized representation preserves core execution logic through a three-phase pipeline: (1) Lexical Preprocessing & Normalization: To avoid language-specific dependencies, ParaDroid applies a token-level filter to strip nonexecutable elements. This includes comments, annotations, and string literals. This sanitization ensures that downstream topological extraction operates exclusively on executable logic and remains immune to textual noise. (2) Intra-Procedural CFG Construction: ParaDroid aggregates sequential statements into logical basic blocks (BBs) via scope depth tracking and control-flow keyword recognition. It then computes directed control-flow graphs (cfg) by tracing branch conditions and resolving post-dominator nodes for path convergence. The pipeline performs control-flow normalization by mapping language-specific constructs, such as Kotlin’s non-local returns (return@label) and Elvis operators (?:), into standard execution bounds equivalent to Java control flows. (3) Conservative Call-Edge Resolution: To capture inter-procedural behaviors, ParaDroid extracts explicit function invocations (calls) within each basic block. Since the shallow parser lacks the deep type inference needed to perfectly resolve polymorphic dispatch across different languages, it employs a weakly typed over-approximation strategy. Call sites are resolved to a candidate pool of all matching overloaded signatures. This approach preserves execution completeness and prevents analysis crashes. The resulting UEG is a deterministic, language-neutral JSON topology consisting of method signatures, basic block arrays (cfg), and intra-block invocation edges (calls). This format ensures the downstream LLM analyzes a pure graph representing execution and data flow. This representation is entirely free of Java and Kotlin syntactic disparities. Listing 6 provides an example. 1 { 2 "kotlin_implementation": {
Lost in Migration
3 4 5 6 7 8 9 10
11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 } 27 }
"PermissionService . getAppOpPermissionPackages ( Entry ) ": { "sig": " < fun getAppOpPermissionPackages ( String ) : Array < String > > ", "calls": { "BB_2": [ "AccessCheckingService . getState ( Callee ) " ], "BB_4": [ "AppIdPermissionPolicy . getPermissions ( Callee )" ] }, "cfg": { "BB_0": [ "override fun getAppOpPermissionPackages ( permissionName: String ) : Array < String > {\ nrequireNotNull ( permissionName ) { ", [ "BB_1" ]] , "BB_1": ["\ "permissionName cannot be null \ "", [ "BB_2" ]] , "BB_2": [ "val packageNames = ArraySet < String >() \ nval permission = service . getState { ", [ "BB_3" ]] , "BB_3": [ "with ( policy ) { ", [ "BB_4" ]] , "BB_4": [ "getPermissions () [ permissionName ] ", [ "BB_5" ]] , "BB_5": [ "if ( permission == null || ! permission . isAppOp ) { ", [ "BB_6", "BB_7" ]] , "BB_6": [ "packageNames . toTypedArray () ", [ "BB_7" ]] , "BB_7": [ "packageManagerLocal . withUnfilteredSnapshot () . use { snapshot -> ", [ "BB_8" ]] , "BB_8": [ "snapshot . packageStates . forEach packageStates @ { ( _, packageState ) -> ", [ "BB_9", "BB_12" ]] , "BB_9": [ "val androidPackage = packageState . androidPackage ? : return @ packageStates", [ "BB_10", "BB_8" ]] , "BB_10": [ "if ( permissionName in androidPackage . requestedPermissions ) { ", [ "BB_11", "BB_8" ]] , "BB_11": [ "packageNames += androidPackage . packageName", [ "BB_8" ]] , "BB_12": [ "return packageNames . toTypedArray () ", [ "End" ]] } }, ... // Omit other parts
Listing 6: Partial UEG for getAppOpPermissionPackages (Kotlin version). Behavioral Divergence Detection. Following UEG normalization, ParaDroid performs a comparative analysis. We prompt the LLM, acting as a Principal Android OS Engineer, to identify structural and behavioral divergences. During this phase, the LLM traverses the paired UEGs, tracking cross-boundary data propagation via cfg and calls edges to align execution paths. To handle migration-induced structural shifts, semantic N:M mapping guides the LLM to group related blocks by computational intent rather than syntactic boundaries. Since refactoring often alters method boundaries, the LLM performs a crossboundary search across the entire cfg pool rather than restricting matches to identically named methods. Guided by this alignment, the LLM extracts semantic signals across four computational deviations. First, guard & state integrity identifies discrepancies where one implementation enforces strict admission checks before state mutations while the other bypasses them. Second, fail-open fall-throughs detect explicit aborts like throw or return reduced to non-blocking operations. Third, data
resolution & terminal state asymmetry distinguishes outcomes derived from static scalars versus dynamic runtime evaluations. Fourth, core logic deviations capture misaligned mathematical operations, differing loop boundaries, or altered exception-handling ranges. Finally, ParaDroid constrains the LLM to output a Structured Divergence Record (SDR) in JSON format for each deviation, as the example in Listing 7. This object contains a reasoning trace documenting call chains, mapped blocks, and extracted guard conditions. The LLM grounds every discrepancy description purely in observable code facts. Each explanation is tightly anchored to specific basic block IDs, such as BB_4 in Java versus BB_6 in Kotlin. These explicit coordinates provide traceable evidence for the subsequent validation phase. 1 { 2 "divergence_id": "div_2", 3 "analyzed_java_entry_method": "public String [] getAppOpPermissionPackages ( @ NonNull String permissionName ) ", 4 "analyzed_kotlin_entry_method": "override fun getAppOpPermissionPackages ( permissionName: String ) : Array < String > ", 5 "java_path_involved": [ 6 "PermissionManagerServiceImpl . getAppOpPermissionPackagesInternal::BB_1", 7 "PermissionManagerServiceImpl . getAppOpPermissionPackagesInternal::BB_2" 8 ], 9 "kotlin_path_involved": [ 10 "PermissionService . getAppOpPermissionPackages::BB_5", 11 "PermissionService . getAppOpPermissionPackages::BB_6", 12 "PermissionService . getAppOpPermissionPackages::BB_7" 13 ] , 14 "neutral_divergence_description": "Fail - Open Guard Logic: In Java, if the permission is not present in the AppOp registry, it returns ' EmptyArray . STRING ' immediately ( BB_2 ) . In Kotlin, the check ' if ( permission == null || ! permission . isAppOp ) ' ( BB_5 ) is effectively ignored ; the flow proceeds from BB_6 into the full package scan ( BB_7 ), returning packages that request the name regardless of its AppOp status . " 15 }
Listing 7: Partial SDR for getAppOpPermissionPackages. Security Assessment & Threat Modeling. Not all behavioral divergences translate to security vulnerabilities. To filter out benign variations and objectively evaluate risks, ParaDroid initiates a twostep Security Assessment phase for the generated SDRs. (1) Contextual Consistency Verification: The LLM first validates SDR authenticity to prevent hallucinations by querying the provided block IDs against the original UEG. This confirms whether the semantic intent genuinely changed. The LLM discards the divergence if architectural refactoring merely moved the logic to a deeper sub-callee or if structural shifts do not affect the actual data flow. (2) RAG-Enhanced Threat Modeling & Assessment: For verified divergences, the model evaluates if the discrepancy constitutes a newly introduced Kotlin regression or an unpatched legacy Java flaw. To assess exploitability and prevent subjective overestimation, ParaDroid utilizes a Retrieval-Augmented Generation (RAG) mechanism. This mechanism injects the official Android Security Severity Criteria [1] into the LLM context. The augmented LLM systematically analyzes divergence across core threat-modeling
Rui Li, Wenrui Diao, and Debin Gao
dimensions. These dimensions span unprivileged IPC reachability, the integrity of global OS states, internal metadata exposure, and critical execution anomalies like TOCTOU races or system-wide DoS. By grounding its reasoning in these unified rules and official metrics, the LLM maps the vulnerability to an objective severity tier (Critical, High, Moderate, Low, or None) and discards issues with negligible impacts. Following the assessment, ParaDroid outputs a security assessment report detailing the severity reasoning and exploit rationale, as shown in Listing 8. To mitigate LLM non-determinism, the system determines the final evaluation via majority voting (2/3 consensus). 1 { 2 "is_vulnerable": true, 3 "affected_implementation": "Kotlin ( Regression ) ", 4 "vulnerability_type": "InfoLeak", 5 "severity_rating": "Low", 6 "severity_reasoning": "Leaking sensitive system metadata, orphaned states, or historical app footprints . ", 7 "exploit_rationale": "An attacker queries ' getAppOpPermissionPackages ' with a known non AppOp permission string ( e . g . , a custom permission exclusively used by a targeted banking or social app ) . Because the Kotlin implementation lacks the ' return ' keyword during its validation phase, the early - exit check fails open . The OS then actively scans the unfiltered snapshot and returns the names of all packages declaring that permission . This allows an unprivileged attacker to reliably enumerate installed packages on the device, fully bypassing Package Visibility ( QUERY_ALL_PACKAGES ) sandbox restrictions . " 8 }
Listing 8: Partial report for getAppOpPermissionPackages. Remarks: Systematic Hallucination Mitigation. Applying LLMs to complex program analysis risks producing speculative or fabricated findings. To ensure highly reliable vulnerability reports, ParaDroid employs a five-layer defense-in-depth mitigation strategy. (1) Structural Grounding restricts the LLM to the languageagnostic UEG rather than raw source code. This removes syntactic ambiguities that often mislead generative models. (2) Constrained Chain-of-Thought (CoT) requires the output of intermediate reasoning states, such as the reasoning_trace in the SDR. The LLM must explicitly document call stacks, block mappings, and unrolled guard conditions before formulating descriptions. This anchors deductions in observable graph facts instead of latent textual bias. (3) Anchor-Based Contextual Verification limits the LLM to a targeted inspection bounded by basic block IDs in the SDR. Verifying these anchors against entry methods effectively constrains the generation space. (4) RAG-Grounded Threat Modeling injects the official Android Security Severity Criteria as an external source of truth. This prevents the model from subjectively inventing threat scenarios by forcing it to evaluate exploitability against objective rules. (5) Ensemble Consensus executes three independent parallel inferences during threat modeling. A strict majority-voting mechanism requires at least a 2/3 consensus on both the "is_vulnerable" boolean and the severity rating. This statistical agreement neutralizes non-deterministic outliers and single-run errors, ensuring the system reports only reproducible vulnerabilities.
5
Evaluation and Results
We evaluated ParaDroid on AOSP to assess the security implications of parallel implementations. Specifically, this evaluation focuses on the following three research questions. RQ1: How sound is the workflow design of ParaDroid? RQ2: What empirical findings does ParaDroid reveal within the AOSP codebase? RQ3: What are the practical security implications of the discovered vulnerabilities?
5.1
Implementation and Experiment Setup
Prototype Implementation. We implemented a prototype of ParaDroid comprising 7,092 lines of code (2,761 in Java, 2,674 in Python, and 1,657 in JavaScript). ParaDroid integrates dexdump for DEX parsing and Soot for bytecode analysis, while leveraging javalang and kopyt for source parsing. Experimental Data. We conducted our evaluation using the AOSP codebases for Android 14, 15, and 16. The specific build tags are 14.0.0_r37 (141,988 Java & Kotlin files), 15.0.0_r34 (219,347 files), and 16.0.0_r3 (226,873 files). We compiled userdebug images for each version. This compilation target preserves the debugging metadata necessary for accurate class-to-source mapping. Additionally, we manually constructed a ground truth dataset for LLM selection and ablation experiments. This dataset contains 20 selected parallel method pairs. It encompasses 51 verified behavioral divergences. 9 of these cases are confirmed as security-relevant. Execution Environment. We conducted the experiment on an Ubuntu 22.04 server equipped with a 96-core AMD EPYC 9654 CPU and 1 TB of RAM. Usage of LLM. This work utilized the Gemini 3.1 Pro model via the official Python SDK and implemented parallelization to improve query throughput. Preliminary tests against GPT-5.4 and Claude Opus 4.7 motivated this model selection. Evaluations on the groundtruth dataset demonstrated that Gemini 3.1 Pro achieved the best performance. Appendix A details the specific experimental configurations and results. The model likely includes Android codebases in its training data. This exposure improves its understanding of framework code. To reduce costs and comply with rate limits [12], we minimized the input context by retaining only the logic relevant to behavioral equivalence: • We omit boilerplate and utility functions whose internal details are unnecessary for judging equivalence. Examples include add, remove, toString, and println. • If both implementations invoke the same callee, we exclude it from the context. A shared callee inherently does not introduce behavioral divergence. Performance. On average, the end-to-end analysis for a single Android firmware image completed in 5.2 hours. The preliminary stages were executed efficiently. Phase 1 (Data Preparation and Normalization) and Phase 2 (Parallel Method Identification) required 57 minutes and 25 minutes, respectively. Phase 3 (Security-Relevant Divergence Detection) accounted for most of the runtime. Within this phase, the LLM-based steps consumed around 3.8 hours. The
Lost in Migration
Table 1: Ablation experiment for security assessment. Steps Baseline (w/o) Verif (w/o) RAG ParaDroid
F1 F1↓ 15.38% 54.62% 17.39% 52.61% 28.57% 41.43% 70.00% -
Recall 25.00% 40.00% 23.53% 58.33%
Recall↓ 33.33% 18.33% 34.80% -
Precision Precision↓ 11.11% 76.39% 11.11% 76.39% 36.36% 51.14% 87.50% -
Code Context Extraction and UEG Construction required less than 10 seconds, introducing negligible overhead. The extended duration of Phase 3 stems primarily from LLM API round-trip latency and batching overhead. The average LLM usage per image was 5.9 million tokens.
5.2
Ablation Experiments
To evaluate the contribution of key Phase 3 components to the overall effectiveness of ParaDroid, we conducted ablation experiments. We specifically analyze UEG Construction, Contextual Consistency Verification, and RAG Enhancement. Impact of UEG on Behavioral Divergence Discovery. The first experiment evaluates whether the UEG representation improves behavioral divergence identification compared to raw source code. We compared the full ParaDroid pipeline against a source-code baseline. In real-world code, the population of non-divergent points is practically infinite and impossible to label exhaustively. Consequently, true negatives remain undefined. This renders recall and F1-score inapplicable for the discovery phase. We instead measure Coverage (the percentage of identified ground-truth divergences) and Accuracy (the percentage of genuine reported divergences). The UEG-based approach identified 47 behavioral divergences, achieving 80.4% coverage and 87.2% accuracy. The baseline identified 58 cases with 64.7% coverage and 56.9% accuracy. These findings confirm that the UEG effectively suppresses syntactic noise and standardizes cross-language control flows. It enables the LLM to focus on core semantic logic. Impact of Verification and RAG on Security Assessment. The second experiment isolates the impact of Contextual Consistency Verification and RAG Enhancement during security assessment. We used the 47 divergences identified in the previous experiment as a fixed set of inputs. This experiment constitutes a binary classification task over a closed set. We therefore report standard precision, recall, and F1-score. The result (Table 1) shows that disabling these components caused significant performance degradation. Removing the verification step led to a surge in false positives. The LLM could no longer cross-check findings against the deterministic UEG. Removing the RAG module caused the LLM to output subjective and inconsistent risk ratings.
5.3
Experimental Results on AOSP
Based on the above experiment setup, we applied ParaDroid to analyze the AOSP Android 14-16 codebases for parallel implementations and their security implications. Results Summary. As summarized in Table 2, ParaDroid identified 329 pairs of parallel methods, with 151 on Android 14, 125 on
Table 2: Summary of analysis results by Android OS. Android Images 14.0.0_r37 15.0.0_r34 16.0.0_r3 Total
Method Pairs Type I Type II 81 70 54 71 53 0 329
Behavioral Divergences
Vulnerable Divergences
189 158 25 372
20 17 0 37
Android 15, and 53 on Android 16. The reduced count on Android 16 may indicate that the migration has reached a later stage. We also observed that parallel methods associated with Type I (2) – Directory Shadowing were mostly related to multimedia functionalities, while those under Type II were mostly permission-related. This suggests that different development teams adopt different naming conventions and migration strategies when transitioning from Java to Kotlin. There appears to be no consistent standard across AOSP development. From these parallel method pairs, ParaDroid identified 372 behavioral divergences. During the Security Assessment phase, it flagged 37 instances as potentially vulnerable. We conducted a comprehensive manual review of these 37 instances and found several duplicates. This duplication occurred for two primary reasons. First, identical vulnerabilities naturally persisted across different Android OS versions (e.g., from Android 14 to 15). Second, the LLM flagged multiple distinct behavioral anomalies within certain complex methods (e.g., a failing guard check followed by an asynchronous state mutation) that originated from the same underlying architectural flaw. After deduplicating overlapping reports, we identified 17 unique, security-relevant divergences. We subjected the 17 unique divergences to strict manual verification through source code auditing and PoC exploit construction. We strictly adhered to our threat model to ensure the constructed attacks explicitly breached Android security boundaries. This review identified 6 false positives that did not constitute actual vulnerabilities. We categorize these false positives into two root causes. Impractical Exploitability (5 instances): The system correctly identified genuine behavioral divergences lacking practical exploitability. The vulnerable logic was either shielded from untrusted IPC paths (lacking accessible API boundaries) or required unrealistic preconditions, such as pre-existing root privileges. Architectural Delegation (1 instance): The LLM failed to strictly adhere to the filtering prompts. It misclassified a benign structural refactoring as a guard bypass because the validation logic was safely delegated to a deeper opaque method. For the remaining 11 distinct divergences, we successfully constructed PoC exploits or verified the flaws within the AOSP source code, confirming them as genuine, exploitable vulnerabilities. Google Confirmed Issues. Following responsible disclosure practices, we reported all 11 verified vulnerabilities to the Google Android Security Team. As listed in Table 3, five issues received official confirmation. All five confirmed cases reside within the Android Permission subsystem. This concentration aligns with expectations. Permission management forms the most security-critical boundary in the OS. Its aggressive Kotlin migration in recent releases makes it highly susceptible to semantic translation errors.
Rui Li, Wenrui Diao, and Debin Gao
Table 3: Summary of Google confirmed issues. No. 1 2 3 4 5
Method onPackageAdded removePermission getAppOpPermissionPackages checkPermission getLegacyPermissionState
Issue Type EoP EoP InfoLeak InfoLeak InfoLeak
Affected Side Java Java Kotlin Kotlin Java
Google classified three confirmed issues as security vulnerabilities. Two represent High-Severity Privilege Escalation (EoP) flaws originating from the legacy Java implementations within the Android 14 path. One constitutes a Low-Severity Information Leakage vulnerability introduced in the Kotlin implementations of Android 15 and 16. Google acknowledged the remaining two cases as functional bugs involving internal metadata exposure. The other six reported cases currently await final triage and confirmation (see Table 5 in the Appendix).
6
Case Studies
In this section, we present two representative cases to demonstrate the security implications of divergences between Java and Kotlin parallel implementations.
6.1
Case 1: EoP via onPackageAdded
A malicious app can escalate its privileges by exploiting inconsistent permission update logic during package updates. This allows it to silently gain sensitive permissions, such as CALL_PHONE. Behavior Divergence. The onPackageAdded method is responsible for managing adjustments to permission registration information in the system that arise from app installation or updates. As shown in Figure 3, in the Java implementation (active in Android 14), this process relies on the createOrUpdate function [9], which updates a permission’s registration information only if the internal flag Permission.mReconciled is set to false. Once this flag is marked as true, the definition remains unchanged in future updates. In contrast, the Kotlin implementation [3] utilizes the addPermissions function, which unconditionally updates the permission registration, ignoring the reconciliation status. Consequently, during permission updates, the Java implementation may preserve the stale (and potentially less restrictive) version, whereas the Kotlin implementation correctly applies the updated definition. This inconsistency enables a dangerous scenario: a normal-level dynamic permission (declared by invoking the addPermission API) initially granted to an app can be silently changed to a dangerous one through app update while retaining its grant status. This kind of escalation would always be blocked for manifest permissions (declared in the app’s manifest), where Android enforces checks to prevent protection level upgrades from normal to dangerous [27]. However, these checks can be bypassed with dynamic permissions and the flawed update flow in the Java implementation. Exploit. We developed a PoC app, app-dynamic, which initially registers a custom dynamic permission com.dynamic.cp with the normal protection level via addPermission. In a subsequent update, the app:
Affected Android OS 14 14 15, 16 15, 16 14
Google Confirmation Vulnerability (High severity) Vulnerability (High severity) Vulnerability (Low severity) Acknowledged as a bug Acknowledged as a bug
CVE ID CVE-2024-43095 CVE-2026-0026 N/A N/A N/A
{ "vulnerable_divergences": [ { "divergence_id": "div_5", "analyzed_java_entry_method": "<Permission createOrUpdate(Permission, PermissionInfo, PackageState, Collection<Permission>, boolean)>", "analyzed_kotlin_entry_method": "<fun MutateStateScope.addPermissions(PackageState, MutableIndexedSet<String>)>", "java_path_involved": [ "Permission.createOrUpdate::BB_8", "Permission.createOrUpdate::BB_11" ], "kotlin_path_involved": [ "AppIdPermissionPolicy.addPermissions::BB_26", "AppIdPermissionPolicy.addPermissions::BB_27" ], "neutral_divergence_description": "When a package is updated and its permission was
already reconciled, Java bypasses updating the permission definition (`mPermissionInfo`). Kotlin explicitly forces an update to the permission definition upon package update by creating a new copy with the updated `PermissionInfo` regardless of reconciliation state.",
"security_impact_assessments": [ { "is_vulnerable": true, "affected_implementation": "Java (Legacy Flaw)", "vulnerability_type": "TOCTOU", "severity_rating": "Moderate", "severity_reasoning": "Local access to protected data (Privileged context) or local arbitrary code execution (Unprivileged)", "exploit_rationale": "1. A victim application pushes an update that upgrades a custom
permission's protection level from 'normal' to 'signature' to remediate a vulnerable IPC interface. 2. A user installs the update. During package scanning, Java's `Permission.createOrUpdate` skips updating `mPermissionInfo` because the permission is already marked as reconciled. 3. The OS retains the stale 'normal' protection level in active memory and fails to trigger the required revocation routines for existing permission holders. 4. An untrusted attacker application installed on the device continues to interface with the supposedly secured component, fully exploiting the prolonged Time-of-Check to Time-of-Use window until the device is eventually rebooted." ]
} ... // Another assessment result is omitted.
} ] }
Figure 3: Report for Case 1.
• Updates com.dynamic.cp to dangerous and assigns it to the PHONE group via addPermission. • Requests both com.dynamic.cp and CALL_PHONE. • Simultaneously declares com.dynamic.cp in its manifest as a normal permission. On a Pixel 7a running Android 14 (Java implementation), we install app-dynamic, run it, and then update the app. After the update, the app automatically obtains the system permission CALL_PHONE during its runtime, without prompting the user. Root Cause. During an app update, Android revokes any permissions that are absent from the updated app manifest. Therefore, to retain the initial grant state (a prerequisite for the escalation), app-dynamic must declare com.dynamic.cp in its updated manifest. In the Java path, the mReconciled flag causes the system
Lost in Migration
{ "vulnerable_divergences": [ { "divergence_id": "div_1", "analyzed_java_entry_method": "<void removePermission(String)>", "analyzed_kotlin_entry_method": "<fun removePermission(String)>", "java_path_involved": [ "PermissionManagerServiceImpl.removePermission::BB_5", "PermissionManagerServiceImpl.removePermission::BB_6", "PermissionManagerServiceImpl.removePermission::BB_7" ], "kotlin_path_involved": [ "PermissionService.removePermission::BB_5", "PermissionService.removePermission::BB_6" ], "neutral_divergence_description": "In the Java implementation, when a non-dynamic
permission is targeted for removal, the system logs a 'WTF' (What a Terrible Failure) but continues execution to successfully remove the permission from the registry. In the Kotlin implementation, the same condition triggers a SecurityException, immediately aborting the execution and preventing the removal of the permission.", "security_impact_assessments": [ { "is_vulnerable": true, "affected_implementation": "Java (Legacy Flaw)", "vulnerability_type": "Integrity Bypass", "severity_rating": "Moderate", "severity_reasoning": "This qualifies as a 'General bypass revealing process
state/metadata across boundaries' or a bypass of system integrity constraints. Specifically, it allows the unauthorized modification (deletion) of static security metadata from the global permission registry.", "exploit_rationale": "An unprivileged attacker application can first call addPermissionTree() to establish ownership over a namespace. Then, the attacker calls removePermission() targeting a non-dynamic permission within that namespace. In the Java implementation, the system acknowledges the operation is 'not allowed' via Slog.wtf() but fails to block the operation, proceeding to delete the permission from the internal mPermissions map. This allows an application to unilaterally alter the system's security configuration by removing static permissions that are supposed to be persistent." ]
} ... // Another assessment result is omitted.
temporary relaxation with a comment: “// TODO: switch this back to SecurityException”. In contrast, the Kotlin implementation acts as a fail-secure guard. Under the same condition (attempting to remove a nondynamic permission), it throws a SecurityException, effectively aborting the operation. Exploit. This behavioral divergence implies that systems running the Java implementation are vulnerable to unauthorized permission deletion, whereas the Kotlin path blocks such attempts. We designed a PoC attack on a Pixel 7a running Android 14. The exploitation proceeds in two steps: (1) Permission Deletion: We developed an attacking-app that invokes removePermission to delete the system-defined READ_VOICEMAIL permission. Due to the Java implementation’s fail-open error handling, the permission is successfully removed from the registry. (2) Ownership Takeover: The app subsequently updates itself to declare READ_VOICEMAIL as a custom permission with a normal protection level (see Listing 9). 1 <! -- Step 2 : Declare the previously system - defined permission as normal -- > 2 < permission android:name = " com . android . voicemail . permission . READ_VOICEMAIL " 3 android:protectionLevel = " normal " > 4 </ permission > 5 < uses - permission android:name = " com . android . voicemail . permission . READ_VOICEMAIL " > 6 </ uses - permission >
}
Listing 9: Manifest snippet of attacking-app.
] }
Figure 4: Report for Case 2.
to ignore this new manifest declaration, because dynamic permissions are always reconciled, leaving the existing dynamic permission object untouched. This allows the subsequent dynamic update (to dangerous) to succeed on the existing object. Conversely, the Kotlin path unconditionally refreshes the permission definition. This causes the manifest declaration to override the dynamic one. Since Android prohibits modifying manifest-defined permissions via addPermission, the subsequent escalation attempt is blocked. Impact. This vulnerability allows a malicious app to silently acquire high-privilege system permissions through standard updates. The issue was confirmed by the Android Security Team and assigned CVE-2024-43095 with a High Severity rating.
6.2
Case 2: EoP via removePermission
In this case, we demonstrate how a discrepancy in error handling between Java and Kotlin leads to a privilege escalation vulnerability. Behavior Divergence. The removePermission method is responsible for removing a permission definition from the system registry. As shown in Figure 4, in the Java implementation (active in Android 14), the logic exhibits a fail-open behavior: it merely logs a warning (Slog.wtf) when encountering a non-dynamic permission but proceeds to execute the deletion. Developers explicitly noted this
Our experiment confirmed that the attacking-app successfully obtained ownership of the READ_VOICEMAIL permission. Consequently, the app accessed the user’s private system voicemail, bypassing the intended access control. This constitutes a privilege escalation vulnerability. Impact. We responsibly disclosed this vulnerability to the Android Security Team. The issue was confirmed and assigned CVE-2026-0026 with a High Severity rating. This case reveals that legacy Java paths, if left active and inconsistent with stricter Kotlin implementations, can serve as latent attack vectors.
7
Discussion
We next discuss broader implications, current limitations, and practical mitigation strategies. Broader Implications: Migration Security. Beyond the specific context of Android, our findings suggest potential security implications for broader software evolution. As the industry increasingly adopts modern languages, such as migrating Linux kernel subsystems to Rust or legacy Objective-C iOS components to Swift, legacy and modern implementations often coexist to support staged rollouts. This coexistence creates a distinct attack surface where subtle semantic gaps between languages, such as in error propagation or type safety, can silently compromise security invariants. Therefore, this work underscores the importance of Migration Security. We position automated differential analysis not merely as a bug-finding technique but as a promising assurance mechanism for the hybrid future of critical software infrastructure.
Rui Li, Wenrui Diao, and Debin Gao
LLM-assisted Analysis. Our approach employs LLMs to evaluate behavioral equivalence across Java and Kotlin. To minimize ambiguity, we provide comprehensive code context, including call chains and method dependencies. To address the probabilistic nature of generative models, we enhance stability through structural grounding, constrained CoT, contextual verification, RAG, and a majority-voting strategy. Although LLMs occasionally produce non-deterministic outputs, combining rich context with consensus mechanisms filters out transient inconsistencies. Consequently, potential inaccuracies primarily manifest as conservative false positives. We mitigate these remaining inaccuracies through manual verification. Mitigation Strategies for Parallel Implementations. Our findings suggest actionable strategies to reduce security risks during language migration. For platform vendors and system developers, we recommend enforcing a strict migration policy that designates a single source of truth for each subsystem. Once the new implementation achieves functional parity, the legacy version should be deprecated and removed immediately. During the transition period, behavioral equivalence should be verified using automated differential testing. This involves executing both versions on identical inputs and strictly comparing their outputs, side effects, and security checks. Additionally, code intended for future versions should be excluded from current production builds to minimize the attack surface. For end users, since these vulnerabilities reside within the system framework, the primary mitigation is to keep devices updated and apply security patches promptly.
8
Related Work
Android security has long been an active research area, and many studies have been conducted [17, 24, 26–30, 34]. We focus our review on related work in Android differential analysis and cross-language security analysis. Differential Analysis. Differential analysis is an effective approach for security analysis. It has been widely used to uncover security flaws introduced by OEM customization, ecosystem fragmentation, and software evolution by comparing a customized or updated target with a trusted baseline. Bandara et al. [18] performed large-scale differential analysis of OEM-customized Android TLS stacks against the AOSP baseline and showed that securitycritical verification logic is frequently modified in ways that can weaken app-level TLS security. Dai et al. [21] proposed ApkDiffer, a two-stage decomposition-based diffing tool that aligns functionally equivalent methods across app versions to reduce alignment errors and enable precise security-relevant change localization. Continella et al. [20] made black-box differential analysis practical for Android privacy leak detection by controlling sensitive inputs and eliminating network nondeterminism to infer leaks from traffic deviations even under obfuscation. Aafer et al. [16] systematically identified customization-sensitive security features and used largescale differential analysis across 591 custom images to find prevalent inconsistencies that they validated as exploitable on real devices. Zhou et al. [37] built ADDICTED to differentially compare device file protections between customized phones and official Android, exposing under-protected driver interfaces that enable unprivileged app attacks. Yang et al. [36] performed differential analysis between
Google Play and third-party market versions of apps claiming the same version code, uncovering widespread security and privacy inconsistencies. Unlike these studies, our work does not perform differential analysis in the conventional setting, which typically assumes a single language or the same target type. Instead, we focus on Java and Kotlin, which introduce a series of challenges from pair identification to security analysis. Cross-language Analysis. The development of Android apps and system components can involve multiple programming languages, including Java, Kotlin, C/C++ (via the NDK), HTML, and JavaScript. Recent work has explored cross-language analysis in this context. For mixed analysis of Java and web programming languages, Hu et al. [23] developed 𝜔Test, a test generation technique for Android WebViews. It applies cross-language dynamic analysis of Java and JavaScript to capture WebView-specific properties and generate event sequences for detecting interaction-related bugs. Tiwari et al. [32] designed a demand-driven analysis framework for Android hybrid apps, which tracks information flows between Java code in the app and embedded JavaScript by selectively summarizing the shared Java code based on its usage in JavaScript. For mixed analysis of Java code and native code (C/C++) in Android apps, Xiong et al. [35] proposed Atlas, a cross-language fuzzing framework for Android closed-source native libraries. It analyzes both Java and native code to generate harnesses and supports fuzzing in an emulator with a Java runtime. Wang et al. [33] introduced NativeSummary, an inter-language static analysis framework for Android apps. It extracts semantics from native C/C++ code, translates JNI usage and native calls into Java bytecode, and extends existing Java analysis tools to support cross-language data flow analysis. Borzacchiello et al. [19] developed DroidReach, a static analysis approach that combines heuristics and symbolic execution to accurately assess the reachability of native C/C++ functions in Android apps, thereby enabling more precise vulnerability assessment. Unlike prior studies that target individual Android applications, our work analyzes system-level Android OS code. Consequently, the identified security issues affect AOSP and broadly impact downstream device vendors.
9
Conclusion
To our knowledge, this paper presents the first systematic security investigation of parallel Java and Kotlin implementations within the Android framework. We designed ParaDroid to automatically discover parallel method pairs and perform cross-language differential analysis. By combining bytecode-level IR, a language-agnostic Unified Execution Graph (UEG), and an LLM-assisted reasoning engine, ParaDroid effectively bridges the semantic gap to identify behavioral deviations. Evaluated on AOSP Android 14–16, ParaDroid identified 372 divergences across 329 method pairs, flagging 37 as potentially vulnerable. Responsible disclosure yielded three confirmed vulnerabilities (with two CVE IDs) and two functional bugs. Ultimately, our findings highlight a critical takeaway: OS-level language migrations are not merely syntax updates, but inherently security-relevant transformations that require systematic semantic consistency checks.
Lost in Migration
Ethical Considerations Experimental Safety. All analyses in this study were conducted in a controlled, isolated environment without interacting with live user devices or external cloud services. This design choice ensured that no end users, applications, or third-party services were exposed to unintended risks or service disruptions during our evaluation. Responsible Disclosure. We have followed a coordinated vulnerability disclosure process with the Android Security Team. To date, five reports have been confirmed: three as vulnerabilities and two as bugs. Two CVE IDs have been assigned: CVE-2024-43095 (high severity) and CVE-2026-0026 (high severity). Google acknowledged our findings by awarding us USD 14,000 through the Android Security Reward Program. Ecosystem Impact Management. We acknowledge that the identified vulnerabilities reside in the core AOSP framework and thus affect the broader Android ecosystem, including downstream vendors (e.g., Samsung, Xiaomi, OPPO). To mitigate the risk of potential exploitation before patches propagate to customized system images, we have strictly adhered to the disclosure timeline and withheld proof-of-concept exploits from the public domain until adequate mitigation measures are available. Research Purpose. We believe that responsibly sharing these findings benefits the community by providing platform maintainers and researchers with the necessary insights to address this emerging class of cross-language security risks.
References [1] Android Security Severity Criteria. https://source.android.com/docs/se curity/overview/updates-resources#severity. [2] Android’s Kotlin-first approach. https://developer.android.com/kotlin /first. [3] AppIdPermissionPolicy.kt. https://cs.android.com/android/platform/su perproject/+/android-14.0.0_r37:frameworks/base/services/permiss ion/java/com/android/server/permission/access/permission/AppIdPe rmissionPolicy.kt. [4] AudioPreview.java. https://cs.android.com/android/platform/superpro ject/+/android-14.0.0_r37:packages/apps/Music/src/com/android/mu sic/AudioPreview.java. [5] AudioPreview.kt. https://cs.android.com/android/platform/superpro ject/+/android-14.0.0_r37:packages/apps/Music/kotlin/src/com/and roid/music/AudioPreview.kt. [6] Commit: c95a176. https://cs.android.com/android/_/android/platfor m/packages/apps/Music/+/c95a176b90aadcb4ef4bc007ba6ec3a50d8ba7 4a. [7] Higher-order functions and lambdas. https://kotlinlang.org/docs/lambda s.html. [8] Kotlin. https://kotlinlang.org/. [9] Permission.java. https://cs.android.com/android/platform/superproje ct/+/android-14.0.0_r37:frameworks/base/services/core/java/com/a ndroid/server/pm/permission/Permission.java. [10] PermissionManagerServiceImpl.java. https://cs.android.com/android/p latform/superproject/+/android-14.0.0_r37:frameworks/base/servic es/core/java/com/android/server/pm/permission/PermissionManagerS erviceImpl.java. [11] PermissionService.kt. https://cs.android.com/android/platform/superp roject/+/android-14.0.0_r37:frameworks/base/services/permission/ java/com/android/server/permission/access/permission/PermissionS ervice.kt. [12] Rate limits. https://ai.google.dev/gemini-api/docs/rate-limits. [13] Submit code changes. https://source.android.com/docs/setup/contribu te/submit-patches. [14] WifiDialog2.kt. https://cs.android.com/android/platform/superproje ct/+/android-14.0.0_r37:packages/apps/Settings/src/com/android/s ettings/wifi/WifiDialog2.kt. [15] WifiDialog.java. https://cs.android.com/android/platform/superproje ct/+/android-14.0.0_r37:packages/apps/Settings/src/com/android/s ettings/wifi/WifiDialog.java.
[16] Yousra Aafer, Xiao Zhang, and Wenliang Du. Harvesting Inconsistent Security Configurations in Custom Android ROMs via Differential Analysis. In Proceedings of the 25th USENIX Security Symposium (USENIX-SEC), Austin, TX, USA, August 10-12, 2016, 2016. [17] Abbas Acar, Güliz Seray Tuncay, Esteban Luques, Harun Oz, Ahmet Aris, and A. Selcuk Uluagac. 50 Shades of Support: A Device-Centric Analysis of Android Security Updates. In Proceedings of the 31st Annual Network and Distributed System Security Symposium (NDSS), San Diego, California, USA, February 26 March 1, 2024, 2024. [18] Vinuri Bandara, Stijn Pletinckx, Ilya Grishchenko, Christopher Kruegel, Giovanni Vigna, Juan Tapiador, and Narseo Vallina-Rodriguez. Beneath the Surface: An Analysis of OEM Customizations on the Android TLS Protocol Stack. In Proceedings of the 10th IEEE European Symposium on Security and Privacy (EuroS&P), Venice, Italy, June 30 - July 4, 2025, 2025. [19] Luca Borzacchiello, Emilio Coppa, Davide Maiorca, Andrea Columbu, Camil Demetrescu, and Giorgio Giacinto. Reach Me if You Can: On Native Vulnerability Reachability in Android Apps. In Proceedings of the 27th European Symposium on Research in Computer Security, Copenhagen, Denmark, September 26-30, 2022, 2022. [20] Andrea Continella, Yanick Fratantonio, Martina Lindorfer, Alessandro Puccetti, Ali Zand, Christopher Kruegel, and Giovanni Vigna. Obfuscation-Resilient Privacy Leak Detection for Mobile Apps Through Differential Analysis. In Proceedings of the 24th Annual Network and Distributed System Security Symposium (NDSS), San Diego, California, USA, February 26 - March 1, 2017, 2017. [21] Jiarun Dai, Mingyuan Luo, Yuan Zhang, Min Yang, and Minghui Yang. ApkDiffer: Accurate and Scalable Cross-Version Diffing Analysis for Android Applications. Proc. ACM Program. Lang., 9(OOPSLA2), 2025. [22] Android Developers. Migrating the AOSP QuickSearchBox App to Kotlin. https: //medium.com/androiddevelopers/migrating-the-aosp-quicksearchb ox-app-to-kotlin-1264346619ec. [23] Jiajun Hu, Lili Wei, Yepang Liu, and Shing-Chi Cheung. 𝜔 Test: WebViewOriented Testing for Android Applications. In René Just and Gordon Fraser, editors, Proceedings of the 32nd ACM SIGSOFT International Symposium on Software Testing and Analysis (ISSTA), Seattle, WA, USA, July 17-21, 2023, 2023. [24] Sigmund Albert Gorski III, Seaver Thorn, William Enck, and Haining Chen. FReD: Identifying File Re-Delegation in Android System Services. In Proceedings of the 31st USENIX Security Symposium (USENIX-SEC), Boston, MA, USA, August 10-12, 2022, 2022. [25] Israel Chidera. Kotlin VS Java – What’s the Difference? https://www.freeco decamp.org/news/kotlin-vs-java-whats-the-difference/. [26] Yuede Ji, Mohamed Elsabagh, Ryan Johnson, and Angelos Stavrou. DEFInit: An Analysis of Exposed Android Init Routines. In Proceedings of the 30th USENIX Security Symposium, USENIX Security 2021, August 11-13, 2021, 2021. [27] Rui Li, Wenrui Diao, Zhou Li, Jianqi Du, and Shanqing Guo. Android Custom Permissions Demystified: From Privilege Escalation to Design Shortcomings. In Proceedings of the 42nd IEEE Symposium on Security and Privacy (IEEE S&P), San Francisco, CA, USA, 24-27 May 2021, 2021. [28] Rui Li, Wenrui Diao, Shishuai Yang, Xiangyu Liu, Shanqing Guo, and Kehuan Zhang. Lost in Conversion: Exploit Data Structure Conversion with Attribute Loss to Break Android Systems. In Proceedings of the 32nd USENIX Security Symposium, USENIX Security 2023, Anaheim, CA, USA, August 9-11, 2023, 2023. [29] Baozheng Liu, Chao Zhang, Guang Gong, Yishun Zeng, Haifeng Ruan, and Jianwei Zhuge. FANS: Fuzzing Android Native System Services via Automated Interface Analysis. In Proceedings of the 29th USENIX Security Symposium (USENIXSEC), August 12-14, 2020. [30] Lukas Maar, Florian Draschbacher, Lukas Lamster, and Stefan Mangard. Defectsin-Depth: Analyzing the Integration of Effective Defenses against One-Day Exploits in Android Kernels. In Proceedings of the 33rd USENIX Security Symposium (USENIX-SEC), Philadelphia, PA, USA, August 14-16, 2024, 2024. [31] Matias Martinez and Bruno Gois Mateus. Why Did Developers Migrate Android Applications From Java to Kotlin? IEEE Transactions on Software Engineering, 48(11):4521–4534, 2022. [32] Abhishek Tiwari, Jyoti Prakash, and Christian Hammer. Demand-driven information flow analysis of webview in android hybrid apps. In Proceedings of the 34th IEEE International Symposium on Software Reliability Engineering (ISSRE), Florence, Italy, October 9-12, 2023, 2023. [33] Jikai Wang and Haoyu Wang. NativeSummary: Summarizing Native Binary Code for Inter-language Static Analysis of Android Apps. In Proceedings of the 33rd ACM SIGSOFT International Symposium on Software Testing and Analysis (ISSTA), Vienna, Austria, September 16-20, 2024, 2024. [34] Xiaobo Xiang, Ren Zhang, Hanxiang Wen, Xiaorui Gong, and Baoxu Liu. Ghost in the Binder: Binder Transaction Redirection Attacks in Android System Services. In Proceedings of the 2021 ACM SIGSAC Conference on Computer and Communications Security, Virtual Event, Republic of Korea, November 15 - 19, 2021, 2021. [35] Hao Xiong, Qinming Dai, Rui Chang, Mingran Qiu, Renxiang Wang, Wenbo Shen, and Yajin Zhou. Atlas: Automating Cross-Language Fuzzing on Android
Rui Li, Wenrui Diao, and Debin Gao
Closed-Source Libraries. In Proceedings of the 33rd ACM SIGSOFT International Symposium on Software Testing and Analysis (ISSTA), Vienna, Austria, September 16-20, 2024, 2024. [36] Shishuai Yang, Guangdong Bai, Ruoyan Lin, Jialong Guo, and Wenrui Diao. Beyond the Horizon: Exploring Cross-Market Security Discrepancies in Parallel Android Apps. In Proceedings of the 35th IEEE International Symposium on Software Reliability Engineering (ISSRE), Tsukuba, Japan, October 28-31, 2024, 2024. [37] Xiao-yong Zhou, Yeonjoon Lee, Nan Zhang, Muhammad Naveed, and XiaoFeng Wang. The Peril of Fragmentation: Security Hazards in Android Device Driver Customizations. In Proceedings of the 35th IEEE Symposium on Security and Privacy (Oakland), Berkeley, CA, USA, May 18-21, 2014, 2014.
A
LLM Selection Experiment
To select the most appropriate Large Language Model (LLM) for our highly specific code analysis tasks, we conducted an empirical comparison using the ground-truth dataset established in Section 5.2. Specifically, we evaluated the performance of three state-of-the-art models: Gemini 3.1 Pro, GPT-5.4, and Claude Opus 4.7. All candidate models were integrated into the ParaDroid pipeline and tasked with processing the same Unified Execution Graphs (UEGs) under identical prompt constraints. We specifically restricted our comparative evaluation to the Divergence Identification stage. The rationale is straightforward: accurate divergence extraction is a strict prerequisite for downstream threat modeling. If an LLM generates excessive structural noise or fails to properly align the graphs at this foundational stage, its subsequent security assessments become inherently unreliable. Therefore, we measured the Coverage (the percentage of genuine ground-truth divergences identified) and Accuracy (the percentage of reported divergences that are actually genuine). The experimental results are detailed in Table 4. While Claude Opus 4.7 achieved the highest coverage (92.2%), it exhibited a remarkably low accuracy (59.5%), suggesting it aggressively hallucinated non-existent semantic divergences. GPT-5.4 performed poorly across both metrics. Gemini 3.1 Pro provided the optimal balance,
achieving a highly competitive coverage (80.4%) while decisively leading in accuracy (87.2%). Because minimizing false positives early in the analysis pipeline is critical to preventing alert fatigue and cascading errors during the threat modeling phase, Gemini 3.1 Pro was selected as the default inference engine for ParaDroid.
B
The Supplementary Results
As discussed in Section 5.3, manual inspection and deduplication identified 11 vulnerable divergences. Table 3 details the issues officially confirmed by the Android Security Team. Table 5 catalogs the remaining issues currently under investigation or pending final resolution. This inclusion provides a comprehensive view of the discovery scale achieved by ParaDroid. Table 4: Performance comparison of candidate LLMs. Model Gemini 3.1 Pro GPT-5.4 Claude Opus 4.7
Coverage 80.4% 70.6% 92.2%
Accuracy 87.2% 41.9% 59.5%
Table 5: Summary of cases under investigation. No. Method 6 7 8 9 10 11
Issue Type InfoLeak
getAllowlistedRestrictedPermissions getAllPermissionInfoLeak Groups isPermissionRevoked- InfoLeak ByPolicy onPackageAdded EoP onPackageAdded EoP systemReady EoP
Affected Side Kotlin
Affected Android OS 15, 16
Java
14
Kotlin
15, 16
Java Kotlin Kotlin
14 15, 16 15, 16