In the last articles of the series, we discussed memory security and thread safety in Rust. In this last article, we will look at the implications of a real Rust application using the example of the Quantum CSS project .The CSS engine applies the CSS rules on the page. This is a downstream process that descends the DOM tree, after calculating the parent CSS, child styles can be calculated independently: ideal for parallel computing. By 2017, Mozilla made two attempts to parallelize the style system using C ++. Both failed.
Quantum CSS development has begun to improve performance. Improving security is just a good side effect.
')
There is a definite connection between memory protection and information security bugs. Therefore, we expected Rust to reduce the attack surface in Firefox. This article will look at potential vulnerabilities that have been identified in the CSS engine since the initial release of Firefox in 2002. Then look at what could and couldn’t have been prevented with Rust.
For all the time in the CSS component of Firefox 69 security errors were detected. If we had a time machine and we could write Rust from the very beginning, then 51 (73.9%) error would have become impossible. Although Rust makes it easier to write good code, it also does not provide absolute protection.
Rusty
Rust is a modern system programming language, safe for types and memory. As a side effect of these security guarantees, Rust programs are also thread-safe during compilation. Thus, Rust is particularly well suited for:
- secure processing of unreliable incoming data;
- concurrency to improve performance;
- integration of individual components into the existing code base.
However, Rust does not explicitly fix some classes of errors, especially errors of correctness. In fact, when our engineers rewrote Quantum CSS, they accidentally repeated a critical security bug that had previously been fixed in C ++ code, they accidentally deleted fix
641731 , which allows global history to leak through SVG. Error registered again as a
bug 1420001 . History leakage is rated as a critical security vulnerability. The initial correction was an additional check to see if the document is an SVG image. Unfortunately, this check was missed when rewriting the code.
Although automated tests should find violations of the rules
:visited
like this, in practice they did not detect this error. To speed up automated tests, we temporarily disabled the mechanism that tested this feature — tests are not particularly useful if they are not performed. The risk of re-implementing logical errors can be reduced by good test coverage. But there is still the danger of new logical errors.
As the developer becomes familiar with Rust, his code becomes even more secure. Although Rust does not prevent all possible vulnerabilities, it eliminates a whole class of the most serious bugs.
Quantum CSS security bugs
In general, by default, Rust prevents memory errors, bounds, zero / uninitialized variables, and integer overflow. The nonstandard bug mentioned above remains possible: it crashes due to failed memory allocation.
Security bugs by category
- Memory: 32
- Borders: 12
- Implementation: 12
- Null: 7
- Stack Overflow: 3
- Integer overflow: 2
- Other: 1
In our analysis, all security bugs are related, but only 43 have been officially assessed (it is assigned by Mozilla security engineers on the basis of qualified assumptions about “exploitation”). Regular bugs may indicate missing functions or some kind of failures that do not necessarily lead to data leakage or behavior changes. Official security errors range from low importance (if there is a strong limitation on the attack surface) to critical vulnerability (it may allow an attacker to run arbitrary code on the user's platform).
Memory vulnerabilities are often classified as serious security issues. Of the 34 critical / serious problems, 32 were memory related.
Distribution of security bugs by severity
- Total: 70
- Security Errors: 43
- Critical / Serious: 34
- Fixed Rust: 32
Comparing Rust and C ++
Bug 955913 - Heap buffer overflow in
GetCustomPropertyNameAt
function. The code used the wrong variable for indexing, which led to the interpretation of memory after the end of the array. This can cause a crash when accessing a bad pointer or copying memory into a string that is passed to another component.
The order of all
CSS properties (including custom, that is, custom) is stored in the
mOrder
array. Each element is represented either by the value of the CSS property or, in the case of custom properties, by a value that starts with
eCSSProperty_COUNT
(the total number of non-custom CSS properties). To get the name of custom properties, you first need to get a value from
mOrder
, and then access the name in the appropriate index of the
mVariableOrder
array, which stores the names of custom properties in order.
Vulnerable C ++ code:
void GetCustomPropertyNameAt(uint32_t aIndex, nsAString& aResult) const { MOZ_ASSERT(mOrder[aIndex] >= eCSSProperty_COUNT); aResult.Truncate(); aResult.AppendLiteral("var-"); aResult.Append(mVariableOrder[aIndex]);
The problem occurs in line 6 when using
aIndex
to access the element of the
mVariableOrder
array. The fact is that
aIndex
should be used with the array
mOrder
, not
mVariableOrder
. The corresponding element for the custom property represented by
aIndex
in
mOrder
is actually
mOrder[aIndex] - eCSSProperty_COUNT
.
Corrected C ++ code:
void Get CustomPropertyNameAt(uint32_t aIndex, nsAString& aResult) const { MOZ_ASSERT(mOrder[aIndex] >= eCSSProperty_COUNT); uint32_t variableIndex = mOrder[aIndex] - eCSSProperty_COUNT; aResult.Truncate(); aResult.AppendLiteral("var-"); aResult.Append(mVariableOrder[variableIndex]); }
Corresponding Rust Code
Although Rust is somewhat similar to C ++, it uses other abstractions and data structures. The Rust code will be very different from C ++ (see below for details). First, let's consider what happens if we translate the vulnerable code as literally as possible:
fn GetCustomPropertyNameAt(&self, aIndex: usize) -> String { assert!(self.mOrder[aIndex] >= self.eCSSProperty_COUNT); let mut result = "var-".to_string(); result += &self.mVariableOrder[aIndex]; result }
The Rust compiler will accept such code, because the length of the vectors cannot be determined before execution. Unlike arrays, the length of which must be known, the
Vec type in Rust has a dynamic size. However, in the implementation of the vector of the standard library built-in check boundaries. When an invalid index appears, the program immediately terminates in a controlled manner, preventing any unauthorized access.
The real code in Quantum CSS uses very different data structures, so there is no exact equivalent. For example, we use the powerful built-in Rust data structures to unify the ordering and property names. This eliminates the need to maintain two independent arrays. Rust data structures also improve data encapsulation and reduce the likelihood of such logical errors. Since the code must interact with C ++ code in other parts of the browser, the new
GetCustomPropertyNameAt
function
GetCustomPropertyNameAt
not look like idiomatic Rust code. But it still gives all the security guarantees, while providing a more understandable abstraction of the underlying data.
tl; dr
Since vulnerabilities are often related to memory security breaches, Rust code should significantly reduce the number of critical
CVEs . But even Rust is not perfect. Developers still need to track correctness errors and data leakage attacks. Code review, tests and fuzzing are still needed to support secure libraries.
Compilers cannot catch all programmer errors. Nevertheless, Rust removes the memory security burden from our shoulders, allowing us to focus on the logical correctness of the code.