The correctness of the code on the Android platform is the most important aspect in the context of the security, stability and quality of each Android release. Memory-safety bugs found in C and C ++ code are still the hardest to clean up. Google invests tremendous efforts and resources in detecting, eliminating bugs of this kind, as well as reducing the harm from them, trying to keep bugs in Android releases as few as possible. However, despite all these measures, bugs related to memory safety remain a major source of stability problems. They invariably account for ~70% most serious Android vulnerabilities.
As well as current and planned With efforts to improve the detection of memory-related bugs, Google is also stepping up its efforts to prevent them. Memory-safe languages are the most efficient and cost-effective means of solving this problem. The Android Open Source Project (AOSP) now supports the memory-safe Java and Kotlin languages, Rust, for developing the operating system itself.
Managed languages such as Java and Kotlin are best suited for developing Android applications. These languages were designed with ease of use, portability, and security in mind. Android Runtime (ART) manages memory the way the developer pointed out. The Android operating system uses Java extensively, effectively protecting large chunks of the Android platform from memory bugs. Unfortunately, at low levels of Android OS, Java and Kotlin are powerless.
At low OS levels, systems programming languages such as C, C ++, and Rust are needed. Controllability and predictability were given priority in the design of these languages. They provide access to low-level system resources and hardware. They manage with minimal resources, and it is also relatively easy to predict their performance.
When working with C and C ++, the developer is responsible for managing the lifetime of the memory. Unfortunately, with this kind of work easy to make mistakesespecially when working with complex and multi-threaded codebases.
Rust provides guarantees about memory safety by using a combination of compile-time checks to enforce object lifetime and ownership, as well as run-time checks, to ensure that memory accesses are valid. This security is indeed ensured, and the performance remains no less than when working with C and C ++.
The limits of working in a sandbox
C and C ++ do not offer the same security guarantees as Rust and require strong isolation. All Android processes are sandboxed and we stick to rule of twowhen deciding whether a particular functionality requires additional isolation and privilege reduction. The rule of two is simple: given the following three options, developers can only choose two of them.
On Android, this means that if the code is written in C / C ++ and parses potentially unsafe input, then it must be contained in a hard-coded sandbox with no privileges. Whereas following the rule of two is good at helping to reduce the severity and increase the availability of security vulnerabilities, it comes with some limitations. Sandboxing is expensive; to ensure it requires new processes that incur additional overhead costs and cause delaysrelated to interprocess communication and additional memory consumption. Working in a sandbox does not completely eliminate vulnerabilities in the code, and the efficiency of such work decreases when high bug densityallowing attackers to concatenate multiple vulnerabilities at once.
A memory-safe language like Rust can help overcome these limitations in two ways:
Reduces the density of bugs in our code, thereby increasing the efficiency of the applied sandbox.
Reduces the need for sandboxing as it provides new capabilities that are safer to use and less resource-intensive at the same time.
What about all the C ++ available?
Of course, if we introduce a new programming language, it will not help us in any way with fixing existing bugs in the existing C / C ++ code.
The above analysis of the age of memory safety bugs (measured from the moment they first appeared) allows us to judge why the Android team is focusing on new developments rather than rewriting mature code in C / C ++. Most of the bugs occur in new or recently changed code, and about 50% of bugs are less than a year old.
It may surprise some that relatively old memory bugs are so rare, but it turned out that the old code just does not require emergency intervention. Over time, software bugs are identified and corrected, so it is expected that code that is supported but not actively developed will eventually be completely cleaned of bugs. Reducing the number and density of bugs increases both the efficiency of working in the sandbox and the efficiency of detecting bugs.
Limitations on finding bugs
Identifying bugs with reliable testing, cleaning and fuzzing is critically important for improving the quality and correctness of any programs, including those written in Rust. The key limitation faced by the most effective memory safety checks in this case is that the error condition must be triggered in the instrumented code, and only then can it be noticed. Even in code that is well covered with tests and fuzzing, many bugs slip into the database precisely because they could not be provoked.
Each of the above steps is expensive, and if you skip at least one of them, the bug will remain unpatched for some or even all users. When it comes to complex C / C ++ codebases, often only a few specialists can develop and implement a fix, and even with significant efforts to fix bugs, the changes made are incorrect…
Bug detection is most effective when bugs are relatively rare, and it is possible to eliminate dangerous bugs with due urgency as a priority. To improve bug detection, you must first prevent new bugs from entering your codebase.
Prevention comes first
Rust is updating a number of language aspects to improve code correctness:
Memory safety – provided by a combination of compile-time and run-time checks.
Data Competitiveness – prevents data race. Given how easy it is to write efficient, thread-safe code, Rust has adopted the slogan “Fearless Competitiveness“…
A more expressive type system – helps to prevent logical errors in programming (for example, wrappers of a new type, variants of enumerations with content).
References and variables are immutable by default – which helps the developer to follow the secure principle of least privilege. The programmer marks a reference or variable as mutable only if he really intends to make it so. While C ++ has const, this feature is usually used infrequently and inconsistently. In contrast, the Rust compiler helps you avoid accidental mutability annotations by issuing warnings about mutable values that never change.
Improved error handling in standard libraries – potentially failing calls are wrapped in Result, and therefore the compiler requires the user to check for failure even for functions that do not return the required value. This allows you to protect against bugs such as vulnerabilities. Rage Against the Cageresulting from an unhandled error. Providing easy trickle-down of errors by the operator? and by optimizing Result for low overhead, Rust encourages users to write all potentially failing functions in the same style, so they all get the same protection.
Initialization – requires all variables to be initialized before being used. Historically, uninitialized memory vulnerabilities have been the cause of 3-5% of security vulnerabilities in Android. In Android 11, to mitigate this issue, the automatic memory initialization in C / C ++… However, initialization to zero is not always safe, especially for things like return values, and can be a new source of incorrect error handling in this area. Rust requires that any variable be initialized to a full member of its type before being used. This avoids the problem of inadvertently initializing an unsafe value. Like Clang in C / C ++, the Rust compiler is aware of the initialization requirement and avoids the potential performance problems associated with double initialization.
Safer handling of integers – Avoidance cleanup is enabled by default in Rust debug builds, which encourages programmers to specify wrapping_add if they are actually supposed to allow computational overflows, or saturating_add if not. Overflow cleanup should be enabled for all Android assemblies in the future. In addition, explicit casts are used for all conversions of integer types: a developer cannot make an accidental cast during a function call when assigning a value to a variable or when trying to perform arithmetic operations with other types.
Bringing a new language to the Android platform is a big undertaking. There are toolboxes and dependencies that need to be maintained, test infrastructure and hardware that need to be updated. You will also have to additionally train the developers. This project is not for one year. Stay tuned for updates on the Google blog.