[

Open Source

]

Crash-Proof Apps: YinkoShield's JNI Tips for Low-End Devices

OpenSource Semgrep JNI rule-set for security and reliability

Mar 11, 2025

How can an app protect millions of users on 12,000+ different mobile devices, many of them low-end Android phones? This is the challenge YinkoShield tackles every day. YinkoShield's in-app protection secures millions of monthly users across thousands of device models, particularly in Africa, where many users rely on budget smartphones with limited specs.

These low-end devices face tough constraint such as limited memory, slow processors, and costly data connections which can lead to app crashes and security risks if not handled carefully.

In this article, we share YinkoShield's hard-earned tips for building crash-proof apps using a defensive JNI (Java Native Interface) programming approach, and OpenSource our new set of Semgrep (https://semgrep.dev/) rules that help developers avoid common JNI pitfalls.

The goal: better performance and stronger security, even on the most humble devices, aiming for less than 0.1% crash rates across thousands of device models.

The Low-End Device Challenge

Mobile developers often focus on the latest high-end phones, but in African markets many users have devices with minimal hardware. For example, cheap smartphones often have very limited RAM, which prevents them from running many apps smoothly. These memory constraints mean that an inefficient app can easily run out of memory or freeze on a low-end phone [1].

Other challenges include:

Inefficient Memory Management: Budget devices might only have 1-2 GB of RAM, and older Android OS versions. If your app uses memory carelessly (e.g. leaks memory or holds onto large objects), the operating system will quickly kill it to free resources. You have to code with extreme memory discipline.

Threading Difficulties: Low-end phones also struggle with heavy multi-threading. A complex background task or mismanaged threads can overwhelm a weak CPU, causing slowdowns or deadlocks. Proper synchronization and minimal thread usage are vital on these devices.

Limited Data and Connectivity: In many emerging markets, data is expensive and connectivity is intermittent [1]. This means apps must handle network interruptions gracefully and avoid large background downloads. A crash or restart due to memory issues might force users to re-download data, a non-starter when every megabyte counts.

Bottom line: On constrained devices, performance issues become reliability issues. If your app is not optimized for low memory and slow chips, it will crash. And a crashing app is not just a user experience problem,  it's a security problem too, since a crash can leave data in inconsistent states or disable critical security checks. As one industry specialist put it, "performance and security aren't just technical requirements they're the foundation of user trust and satisfaction." [2]

In other words, to keep users safe, your app must run smoothly and remain secure. While minimizing native code through JNI (Java Native Interface) can reduce certain attack surfaces and compatibility issues, a complete avoidance strategy requires careful consideration. Security is multifaceted,  threats like the Gigabud Android banking malware demonstrate how attackers exploit Android's Accessibility features to steal credentials [3], which requires defense-in-depth strategies that may include both Java and native components.

Both approaches have tradeoffs: pure Java implementations may be more vulnerable to reverse engineering but easier to maintain, while JNI can provide obfuscation, anti-tampering and anti-reverse engineering benefits but introduces potential memory safety issues and cross-platform challenges. The optimal approach depends on your specific security requirements and threat model.  Consider what assets you're protecting, your performance constraints, and the expertise of your development team. Implement security measures appropriate to your risk profile rather than categorically avoiding or embracing any single technology.

JNI – Powerful Tool, Potential Pitfalls

Many mobile apps (including YinkoShield's security toolkit) use the Java Native Interface (JNI) to call native C/C++ code for performance-critical tasks. JNI is powerful, it lets you do things in C/C++ that Java alone might not handle efficiently but it also comes with zero safety nets. When you write JNI code, you step outside Android's managed memory and into manual memory management, pointers, and explicit error handling. Mistakes here can cause crashes, memory leaks, or even security vulnerabilities.

Why use JNI at all? YinkoShield leverages JNI to implement low-level security functions (like anti-tampering and zero-trust checks) in native code for speed and obfuscation. This helps protect our clients' apps without slowing them down. But to succeed, we had to adopt a defensive programming mindset in JNI:

• We assume nothing will go as expected. Every JNI call might fail or throw an exception, especially on low-end devices with unpredictable states.

• We check every return value and error code. A JNI function that returns a reference could return NULL on error  if we don't check, using that null will crash the app.[4]

• We clean up after ourselves religiously. Unlike in Java, memory allocated in C (or references created via JNI) won't be freed by the garbage collector. Forgetting to free or release them causes leaks that accumulate over time. On a 1GB RAM phone, even a small leak can crash the app after enough usage.

• We avoid doing too much in one go. JNI calls have overhead. Calling into Java in a tight loop from C, or vice versa, can be very slow and consume extra memory for each transition. Wherever possible, we batch operations or cache references to minimize JNI call frequency.

This defensive programming approach paid off, YinkoShield's protection runs on devices so low-end that they barely run stock apps. By carefully managing memory and checking for errors, we dramatically reduced runtime crashes. But we realized that these JNI best practices shouldn't be kept secret. What if every developer could automatically enforce these patterns in their own apps?

Crash-Proofing with Semgrep Rules for JNI

To help the Android developer community, YinkoShield is releasing a set of Semgrep rules that encode our defensive JNI programming best practices. Semgrep is a static analysis tool that scans code for patterns (like a super-charged linter). Our Semgrep rules will catch common JNI mistakes before they cause trouble on a user's device. Here are key patterns our rules check:

Missing Exception Checks

JNI calls can throw Java exceptions (for example, FindClass will throw if the class isn't found). If you don't check and clear the exception on the native side, your native function might continue in a bad state, potentially leading to memory corruption, invalid pointer dereferencing, or unpredictable behavior. Our rule flags any JNI call that isn't followed by an ExceptionCheck/ExceptionClear.

Here's an example of our Semgrep rule that catches missing exception handling:


This rule identifies JNI function calls that aren't followed by proper exception handling, helping developers avoid subtle crash conditions.

Null Return Values Not Checked

Functions like GetMethodID or NewObject return NULL if something went wrong (and typically throw an exception too). Using a null jobject or jclass without checking will cause a crash. The Semgrep rules ensure that every JNI return value that could be null is checked before use.

Memory Leaks (Unreleased References or Allocations)

A classic JNI bug is failing to release resources. Our Semgrep rules catch patterns like:

• Calling NewGlobalRef without a corresponding DeleteGlobalRef

• Extensive use of local references in loops without cleanup

• Native allocations (malloc/new) that are not freed

These patterns are flagged so you can fix them before they cause an out-of-memory crash. Remember, any global reference you create should be deleted when no longer needed otherwise you're effectively creating a memory leak that the Java GC cannot ever reclaim.[5]

Expensive JNI Calls Inside Loops

It's a performance liability to invoke JNI calls in a tight loop when you could do it more efficiently. For instance, calling FindClass repeatedly for the same class or calling a Java method from C on each iteration can be extremely slow on low-end hardware.

Instead of doing this (bad pattern):


Our Semgrep rule would warn that FindClass and GetMethodID are called in a loop and not cached. The better approach is to look up cls and mid before the loop, store them, and reuse. Not only does this avoid performance overhead, it also prevents leaking local references. On a low-end device, saving that extra work can be the difference between a smooth run and a stuttering (or crashing) experience.

Performance and Security: Two Sides of the Same Coin

Performance and security are deeply intertwined in mobile apps. A poorly performing app that crashes or leaks memory is itself a security risk. Why?

Stability = Trust: If your app crashes often, users may disable security features or uninstall it entirely. A fast, crash-proof app maintains user trust.

Resource Exhaustion Vulnerabilities: Memory leaks or heavy CPU usage can be exploited. If an attacker knows your app will crash when memory is low, they might trigger it to load large data (or just wait for natural leaks to accumulate) to cause a failure at a critical moment.

Secure Code Foundations: The same discipline required to manage memory (checking for nulls, handling exceptions, cleaning up) also prevents security bugs. For instance, checking every return code means you're less likely to overlook an error that could lead to undefined behavior.

Google's Android developers guide emphasizes minimizing JNI transitions and careful memory management [6] not just for efficiency, but to avoid the unpredictable bugs that come with unnecessary complexity. In mobile development, strong performance hygiene is a form of preventative security.

Conclusion: Principles of Defensive JNI Programming

Defensive JNI programming is built on several key principles that all developers should incorporate into their work:


  1. Trust nothing: Always validate return values, check for exceptions, and handle error cases explicitly.

  2. Resource discipline: Release every reference you create, free all allocated memory, and avoid resource leaks through careful tracking.

  3. Minimize overhead: Cache method IDs and class references instead of repeatedly looking them up, and batch operations where possible to reduce JNI boundary crossings.

  4. Test on real constraints: Don't just test on high-end devices; validate your app's behavior under memory pressure and on low-spec hardware.

  5. Use automated tools: Static analyzers like Semgrep can catch many common JNI errors before they reach production.


By following these principles, developers can create apps that run reliably even on devices with severe hardware limitations. This not only improves the user experience but also strengthens security by eliminating vulnerability patterns that could be exploited.

The complete set of YinkoShield's Semgrep rules for JNI will be available on our GitHub repository (https://github.com/yinkoshield). We encourage the developer community to contribute additional rules based on their own experiences and to adapt these patterns to their specific use cases.

Building resilient apps isn't just about coding well it's about coding defensively, with awareness of the constraints your users face. Through careful attention to JNI details and systematic error handling, we can create apps that work reliably for everyone, regardless of their device's capabilities.

We invite you to explore these resources and join the conversation on advancing robust, secure app development.

References

[1] https://www.microsave.net/2022/11/07/can-access-to-smartphones-bridge-the-digital-divide-in-sub-saharan-africa/

[2] https://thisisglance.com/learning-centre/what-makes-a-mobile-app-development-project-successful

[3] https://cyble.com/blog/gigabud-rat-new-android-rat-masquerading-as-government-agencies/

[4] https://wykvictor.github.io/2016/02/28/Best-Practices-JNI.html

[5] https://javanexus.com/blog/common-jni-pitfalls-avoid-memory-leaks

[6] https://developer.android.com/training/articles/perf-jni

[

benefits

]

Why choose
in-app protection?

Targeted Protection

YinkoShield's innovative approach focuses on delivering precise, tailored security solutions, ensuring your specific needs are met with unparalleled accuracy.

Seamless Integration

Benefit from YinkoShield's seamless integration that enhances your systems without complication, offering an effortless setup and user-friendly experience.

Advanced Security Features

YinkoShield stands out with its advanced features like true device identity and apartment-level geolocation, setting a new standard for in-app protection.

[

Get started

]

Advance your security journey with us

[

Open Source

]

JNI-Security Rules

[

Open Source

]

JNI-Security Rules

Crash-Proof Apps: YinkoShield's JNI Tips for Low-End Devices

Mar 11, 2025

How can an app protect millions of users on 12,000+ different mobile devices, many of them low-end Android phones? This is the challenge YinkoShield tackles every day. YinkoShield's in-app protection secures millions of monthly users across thousands of device models, particularly in Africa, where many users rely on budget smartphones with limited specs.

These low-end devices face tough constraint such as limited memory, slow processors, and costly data connections which can lead to app crashes and security risks if not handled carefully.

In this article, we share YinkoShield's hard-earned tips for building crash-proof apps using a defensive JNI (Java Native Interface) programming approach, and OpenSource our new set of Semgrep (https://semgrep.dev/) rules that help developers avoid common JNI pitfalls.

The goal: better performance and stronger security, even on the most humble devices, aiming for less than 0.1% crash rates across thousands of device models.

The Low-End Device Challenge

Mobile developers often focus on the latest high-end phones, but in African markets many users have devices with minimal hardware. For example, cheap smartphones often have very limited RAM, which prevents them from running many apps smoothly. These memory constraints mean that an inefficient app can easily run out of memory or freeze on a low-end phone [1].

Other challenges include:

Inefficient Memory Management: Budget devices might only have 1-2 GB of RAM, and older Android OS versions. If your app uses memory carelessly (e.g. leaks memory or holds onto large objects), the operating system will quickly kill it to free resources. You have to code with extreme memory discipline.

Threading Difficulties: Low-end phones also struggle with heavy multi-threading. A complex background task or mismanaged threads can overwhelm a weak CPU, causing slowdowns or deadlocks. Proper synchronization and minimal thread usage are vital on these devices.

Limited Data and Connectivity: In many emerging markets, data is expensive and connectivity is intermittent [1]. This means apps must handle network interruptions gracefully and avoid large background downloads. A crash or restart due to memory issues might force users to re-download data, a non-starter when every megabyte counts.

Bottom line: On constrained devices, performance issues become reliability issues. If your app is not optimized for low memory and slow chips, it will crash. And a crashing app is not just a user experience problem,  it's a security problem too, since a crash can leave data in inconsistent states or disable critical security checks. As one industry specialist put it, "performance and security aren't just technical requirements they're the foundation of user trust and satisfaction." [2]

In other words, to keep users safe, your app must run smoothly and remain secure. While minimizing native code through JNI (Java Native Interface) can reduce certain attack surfaces and compatibility issues, a complete avoidance strategy requires careful consideration. Security is multifaceted,  threats like the Gigabud Android banking malware demonstrate how attackers exploit Android's Accessibility features to steal credentials [3], which requires defense-in-depth strategies that may include both Java and native components.

Both approaches have tradeoffs: pure Java implementations may be more vulnerable to reverse engineering but easier to maintain, while JNI can provide obfuscation, anti-tampering and anti-reverse engineering benefits but introduces potential memory safety issues and cross-platform challenges. The optimal approach depends on your specific security requirements and threat model.  Consider what assets you're protecting, your performance constraints, and the expertise of your development team. Implement security measures appropriate to your risk profile rather than categorically avoiding or embracing any single technology.

JNI – Powerful Tool, Potential Pitfalls

Many mobile apps (including YinkoShield's security toolkit) use the Java Native Interface (JNI) to call native C/C++ code for performance-critical tasks. JNI is powerful, it lets you do things in C/C++ that Java alone might not handle efficiently but it also comes with zero safety nets. When you write JNI code, you step outside Android's managed memory and into manual memory management, pointers, and explicit error handling. Mistakes here can cause crashes, memory leaks, or even security vulnerabilities.

Why use JNI at all? YinkoShield leverages JNI to implement low-level security functions (like anti-tampering and zero-trust checks) in native code for speed and obfuscation. This helps protect our clients' apps without slowing them down. But to succeed, we had to adopt a defensive programming mindset in JNI:

• We assume nothing will go as expected. Every JNI call might fail or throw an exception, especially on low-end devices with unpredictable states.

• We check every return value and error code. A JNI function that returns a reference could return NULL on error  if we don't check, using that null will crash the app.[4]

• We clean up after ourselves religiously. Unlike in Java, memory allocated in C (or references created via JNI) won't be freed by the garbage collector. Forgetting to free or release them causes leaks that accumulate over time. On a 1GB RAM phone, even a small leak can crash the app after enough usage.

• We avoid doing too much in one go. JNI calls have overhead. Calling into Java in a tight loop from C, or vice versa, can be very slow and consume extra memory for each transition. Wherever possible, we batch operations or cache references to minimize JNI call frequency.

This defensive programming approach paid off, YinkoShield's protection runs on devices so low-end that they barely run stock apps. By carefully managing memory and checking for errors, we dramatically reduced runtime crashes. But we realized that these JNI best practices shouldn't be kept secret. What if every developer could automatically enforce these patterns in their own apps?

Crash-Proofing with Semgrep Rules for JNI

To help the Android developer community, YinkoShield is releasing a set of Semgrep rules that encode our defensive JNI programming best practices. Semgrep is a static analysis tool that scans code for patterns (like a super-charged linter). Our Semgrep rules will catch common JNI mistakes before they cause trouble on a user's device. Here are key patterns our rules check:

Missing Exception Checks

JNI calls can throw Java exceptions (for example, FindClass will throw if the class isn't found). If you don't check and clear the exception on the native side, your native function might continue in a bad state, potentially leading to memory corruption, invalid pointer dereferencing, or unpredictable behavior. Our rule flags any JNI call that isn't followed by an ExceptionCheck/ExceptionClear.

Here's an example of our Semgrep rule that catches missing exception handling:


This rule identifies JNI function calls that aren't followed by proper exception handling, helping developers avoid subtle crash conditions.

Null Return Values Not Checked

Functions like GetMethodID or NewObject return NULL if something went wrong (and typically throw an exception too). Using a null jobject or jclass without checking will cause a crash. The Semgrep rules ensure that every JNI return value that could be null is checked before use.

Memory Leaks (Unreleased References or Allocations)

A classic JNI bug is failing to release resources. Our Semgrep rules catch patterns like:

• Calling NewGlobalRef without a corresponding DeleteGlobalRef

• Extensive use of local references in loops without cleanup

• Native allocations (malloc/new) that are not freed

These patterns are flagged so you can fix them before they cause an out-of-memory crash. Remember, any global reference you create should be deleted when no longer needed otherwise you're effectively creating a memory leak that the Java GC cannot ever reclaim.[5]

Expensive JNI Calls Inside Loops

It's a performance liability to invoke JNI calls in a tight loop when you could do it more efficiently. For instance, calling FindClass repeatedly for the same class or calling a Java method from C on each iteration can be extremely slow on low-end hardware.

Instead of doing this (bad pattern):


Our Semgrep rule would warn that FindClass and GetMethodID are called in a loop and not cached. The better approach is to look up cls and mid before the loop, store them, and reuse. Not only does this avoid performance overhead, it also prevents leaking local references. On a low-end device, saving that extra work can be the difference between a smooth run and a stuttering (or crashing) experience.

Performance and Security: Two Sides of the Same Coin

Performance and security are deeply intertwined in mobile apps. A poorly performing app that crashes or leaks memory is itself a security risk. Why?

Stability = Trust: If your app crashes often, users may disable security features or uninstall it entirely. A fast, crash-proof app maintains user trust.

Resource Exhaustion Vulnerabilities: Memory leaks or heavy CPU usage can be exploited. If an attacker knows your app will crash when memory is low, they might trigger it to load large data (or just wait for natural leaks to accumulate) to cause a failure at a critical moment.

Secure Code Foundations: The same discipline required to manage memory (checking for nulls, handling exceptions, cleaning up) also prevents security bugs. For instance, checking every return code means you're less likely to overlook an error that could lead to undefined behavior.

Google's Android developers guide emphasizes minimizing JNI transitions and careful memory management [6] not just for efficiency, but to avoid the unpredictable bugs that come with unnecessary complexity. In mobile development, strong performance hygiene is a form of preventative security.

Conclusion: Principles of Defensive JNI Programming

Defensive JNI programming is built on several key principles that all developers should incorporate into their work:


  1. Trust nothing: Always validate return values, check for exceptions, and handle error cases explicitly.

  2. Resource discipline: Release every reference you create, free all allocated memory, and avoid resource leaks through careful tracking.

  3. Minimize overhead: Cache method IDs and class references instead of repeatedly looking them up, and batch operations where possible to reduce JNI boundary crossings.

  4. Test on real constraints: Don't just test on high-end devices; validate your app's behavior under memory pressure and on low-spec hardware.

  5. Use automated tools: Static analyzers like Semgrep can catch many common JNI errors before they reach production.


By following these principles, developers can create apps that run reliably even on devices with severe hardware limitations. This not only improves the user experience but also strengthens security by eliminating vulnerability patterns that could be exploited.

The complete set of YinkoShield's Semgrep rules for JNI will be available on our GitHub repository (https://github.com/yinkoshield). We encourage the developer community to contribute additional rules based on their own experiences and to adapt these patterns to their specific use cases.

Building resilient apps isn't just about coding well it's about coding defensively, with awareness of the constraints your users face. Through careful attention to JNI details and systematic error handling, we can create apps that work reliably for everyone, regardless of their device's capabilities.

We invite you to explore these resources and join the conversation on advancing robust, secure app development.

References

[1] https://www.microsave.net/2022/11/07/can-access-to-smartphones-bridge-the-digital-divide-in-sub-saharan-africa/

[2] https://thisisglance.com/learning-centre/what-makes-a-mobile-app-development-project-successful

[3] https://cyble.com/blog/gigabud-rat-new-android-rat-masquerading-as-government-agencies/

[4] https://wykvictor.github.io/2016/02/28/Best-Practices-JNI.html

[5] https://javanexus.com/blog/common-jni-pitfalls-avoid-memory-leaks

[6] https://developer.android.com/training/articles/perf-jni

[

benefits

]

[

benefits

]

Why choose
in-app protection?

Targeted Protection

YinkoShield's innovative approach focuses on delivering precise, tailored security solutions, ensuring your specific needs are met with unparalleled accuracy.

Seamless Integration

Benefit from YinkoShield's seamless integration that enhances your systems without complication, offering an effortless setup and user-friendly experience.

Advanced Security Features

YinkoShield stands out with its advanced features like true device identity and apartment-level geolocation, setting a new standard for in-app protection.

[

Get started

]

Advance your security journey with us

[

Get started

]

Advance your security journey with us

[

Open Source

]

[

Open Source

]

Crash-Proof Apps: YinkoShield's JNI Tips for Low-End Devices

Mar 11, 2025

How can an app protect millions of users on 12,000+ different mobile devices, many of them low-end Android phones? This is the challenge YinkoShield tackles every day. YinkoShield's in-app protection secures millions of monthly users across thousands of device models, particularly in Africa, where many users rely on budget smartphones with limited specs.

These low-end devices face tough constraint such as limited memory, slow processors, and costly data connections which can lead to app crashes and security risks if not handled carefully.

In this article, we share YinkoShield's hard-earned tips for building crash-proof apps using a defensive JNI (Java Native Interface) programming approach, and OpenSource our new set of Semgrep (https://semgrep.dev/) rules that help developers avoid common JNI pitfalls.

The goal: better performance and stronger security, even on the most humble devices, aiming for less than 0.1% crash rates across thousands of device models.

The Low-End Device Challenge

Mobile developers often focus on the latest high-end phones, but in African markets many users have devices with minimal hardware. For example, cheap smartphones often have very limited RAM, which prevents them from running many apps smoothly. These memory constraints mean that an inefficient app can easily run out of memory or freeze on a low-end phone [1].

Other challenges include:

Inefficient Memory Management: Budget devices might only have 1-2 GB of RAM, and older Android OS versions. If your app uses memory carelessly (e.g. leaks memory or holds onto large objects), the operating system will quickly kill it to free resources. You have to code with extreme memory discipline.

Threading Difficulties: Low-end phones also struggle with heavy multi-threading. A complex background task or mismanaged threads can overwhelm a weak CPU, causing slowdowns or deadlocks. Proper synchronization and minimal thread usage are vital on these devices.

Limited Data and Connectivity: In many emerging markets, data is expensive and connectivity is intermittent [1]. This means apps must handle network interruptions gracefully and avoid large background downloads. A crash or restart due to memory issues might force users to re-download data, a non-starter when every megabyte counts.

Bottom line: On constrained devices, performance issues become reliability issues. If your app is not optimized for low memory and slow chips, it will crash. And a crashing app is not just a user experience problem,  it's a security problem too, since a crash can leave data in inconsistent states or disable critical security checks. As one industry specialist put it, "performance and security aren't just technical requirements they're the foundation of user trust and satisfaction." [2]

In other words, to keep users safe, your app must run smoothly and remain secure. While minimizing native code through JNI (Java Native Interface) can reduce certain attack surfaces and compatibility issues, a complete avoidance strategy requires careful consideration. Security is multifaceted,  threats like the Gigabud Android banking malware demonstrate how attackers exploit Android's Accessibility features to steal credentials [3], which requires defense-in-depth strategies that may include both Java and native components.

Both approaches have tradeoffs: pure Java implementations may be more vulnerable to reverse engineering but easier to maintain, while JNI can provide obfuscation, anti-tampering and anti-reverse engineering benefits but introduces potential memory safety issues and cross-platform challenges. The optimal approach depends on your specific security requirements and threat model.  Consider what assets you're protecting, your performance constraints, and the expertise of your development team. Implement security measures appropriate to your risk profile rather than categorically avoiding or embracing any single technology.

JNI – Powerful Tool, Potential Pitfalls

Many mobile apps (including YinkoShield's security toolkit) use the Java Native Interface (JNI) to call native C/C++ code for performance-critical tasks. JNI is powerful, it lets you do things in C/C++ that Java alone might not handle efficiently but it also comes with zero safety nets. When you write JNI code, you step outside Android's managed memory and into manual memory management, pointers, and explicit error handling. Mistakes here can cause crashes, memory leaks, or even security vulnerabilities.

Why use JNI at all? YinkoShield leverages JNI to implement low-level security functions (like anti-tampering and zero-trust checks) in native code for speed and obfuscation. This helps protect our clients' apps without slowing them down. But to succeed, we had to adopt a defensive programming mindset in JNI:

• We assume nothing will go as expected. Every JNI call might fail or throw an exception, especially on low-end devices with unpredictable states.

• We check every return value and error code. A JNI function that returns a reference could return NULL on error  if we don't check, using that null will crash the app.[4]

• We clean up after ourselves religiously. Unlike in Java, memory allocated in C (or references created via JNI) won't be freed by the garbage collector. Forgetting to free or release them causes leaks that accumulate over time. On a 1GB RAM phone, even a small leak can crash the app after enough usage.

• We avoid doing too much in one go. JNI calls have overhead. Calling into Java in a tight loop from C, or vice versa, can be very slow and consume extra memory for each transition. Wherever possible, we batch operations or cache references to minimize JNI call frequency.

This defensive programming approach paid off, YinkoShield's protection runs on devices so low-end that they barely run stock apps. By carefully managing memory and checking for errors, we dramatically reduced runtime crashes. But we realized that these JNI best practices shouldn't be kept secret. What if every developer could automatically enforce these patterns in their own apps?

Crash-Proofing with Semgrep Rules for JNI

To help the Android developer community, YinkoShield is releasing a set of Semgrep rules that encode our defensive JNI programming best practices. Semgrep is a static analysis tool that scans code for patterns (like a super-charged linter). Our Semgrep rules will catch common JNI mistakes before they cause trouble on a user's device. Here are key patterns our rules check:

Missing Exception Checks

JNI calls can throw Java exceptions (for example, FindClass will throw if the class isn't found). If you don't check and clear the exception on the native side, your native function might continue in a bad state, potentially leading to memory corruption, invalid pointer dereferencing, or unpredictable behavior. Our rule flags any JNI call that isn't followed by an ExceptionCheck/ExceptionClear.

Here's an example of our Semgrep rule that catches missing exception handling:


This rule identifies JNI function calls that aren't followed by proper exception handling, helping developers avoid subtle crash conditions.

Null Return Values Not Checked

Functions like GetMethodID or NewObject return NULL if something went wrong (and typically throw an exception too). Using a null jobject or jclass without checking will cause a crash. The Semgrep rules ensure that every JNI return value that could be null is checked before use.

Memory Leaks (Unreleased References or Allocations)

A classic JNI bug is failing to release resources. Our Semgrep rules catch patterns like:

• Calling NewGlobalRef without a corresponding DeleteGlobalRef

• Extensive use of local references in loops without cleanup

• Native allocations (malloc/new) that are not freed

These patterns are flagged so you can fix them before they cause an out-of-memory crash. Remember, any global reference you create should be deleted when no longer needed otherwise you're effectively creating a memory leak that the Java GC cannot ever reclaim.[5]

Expensive JNI Calls Inside Loops

It's a performance liability to invoke JNI calls in a tight loop when you could do it more efficiently. For instance, calling FindClass repeatedly for the same class or calling a Java method from C on each iteration can be extremely slow on low-end hardware.

Instead of doing this (bad pattern):


Our Semgrep rule would warn that FindClass and GetMethodID are called in a loop and not cached. The better approach is to look up cls and mid before the loop, store them, and reuse. Not only does this avoid performance overhead, it also prevents leaking local references. On a low-end device, saving that extra work can be the difference between a smooth run and a stuttering (or crashing) experience.

Performance and Security: Two Sides of the Same Coin

Performance and security are deeply intertwined in mobile apps. A poorly performing app that crashes or leaks memory is itself a security risk. Why?

Stability = Trust: If your app crashes often, users may disable security features or uninstall it entirely. A fast, crash-proof app maintains user trust.

Resource Exhaustion Vulnerabilities: Memory leaks or heavy CPU usage can be exploited. If an attacker knows your app will crash when memory is low, they might trigger it to load large data (or just wait for natural leaks to accumulate) to cause a failure at a critical moment.

Secure Code Foundations: The same discipline required to manage memory (checking for nulls, handling exceptions, cleaning up) also prevents security bugs. For instance, checking every return code means you're less likely to overlook an error that could lead to undefined behavior.

Google's Android developers guide emphasizes minimizing JNI transitions and careful memory management [6] not just for efficiency, but to avoid the unpredictable bugs that come with unnecessary complexity. In mobile development, strong performance hygiene is a form of preventative security.

Conclusion: Principles of Defensive JNI Programming

Defensive JNI programming is built on several key principles that all developers should incorporate into their work:


  1. Trust nothing: Always validate return values, check for exceptions, and handle error cases explicitly.

  2. Resource discipline: Release every reference you create, free all allocated memory, and avoid resource leaks through careful tracking.

  3. Minimize overhead: Cache method IDs and class references instead of repeatedly looking them up, and batch operations where possible to reduce JNI boundary crossings.

  4. Test on real constraints: Don't just test on high-end devices; validate your app's behavior under memory pressure and on low-spec hardware.

  5. Use automated tools: Static analyzers like Semgrep can catch many common JNI errors before they reach production.


By following these principles, developers can create apps that run reliably even on devices with severe hardware limitations. This not only improves the user experience but also strengthens security by eliminating vulnerability patterns that could be exploited.

The complete set of YinkoShield's Semgrep rules for JNI will be available on our GitHub repository (https://github.com/yinkoshield). We encourage the developer community to contribute additional rules based on their own experiences and to adapt these patterns to their specific use cases.

Building resilient apps isn't just about coding well it's about coding defensively, with awareness of the constraints your users face. Through careful attention to JNI details and systematic error handling, we can create apps that work reliably for everyone, regardless of their device's capabilities.

We invite you to explore these resources and join the conversation on advancing robust, secure app development.

References

[1] https://www.microsave.net/2022/11/07/can-access-to-smartphones-bridge-the-digital-divide-in-sub-saharan-africa/

[2] https://thisisglance.com/learning-centre/what-makes-a-mobile-app-development-project-successful

[3] https://cyble.com/blog/gigabud-rat-new-android-rat-masquerading-as-government-agencies/

[4] https://wykvictor.github.io/2016/02/28/Best-Practices-JNI.html

[5] https://javanexus.com/blog/common-jni-pitfalls-avoid-memory-leaks

[6] https://developer.android.com/training/articles/perf-jni

[

Get started

]

Advance your security journey with us

[

Get started

]

Advance your security journey with us

[

benefits

]

[

benefits

]

Why choose
in-app protection?

Targeted Protection

YinkoShield's innovative approach focuses on delivering precise, tailored security solutions, ensuring your specific needs are met with unparalleled accuracy.

Seamless Integration

Benefit from YinkoShield's seamless integration that enhances your systems without complication, offering an effortless setup and user-friendly experience.

Advanced Security Features

YinkoShield stands out with its advanced features like true device identity and apartment-level geolocation, setting a new standard for in-app protection.