Abstract: ** In recent years, the Rust language has gained a lot of attention with its rapid growth. It is characterized by high security and performance as good as C/C++. What is the actual security performance of Rust as it is used in many projects?

In recent years, Rust has received a lot of attention for its rapid growth. It is characterized by high security while obtaining C/C++ performance, so that the field of system programming has rarely appeared promising new choices. What is the actual security performance of Rust as it is used in many projects? In June of this year, five scholars from three universities presented a study at the ACM SIGPLAN International Conference (PLDI’20) that investigated the security flaws in open source projects using Rust in recent years.

The study examined five software systems developed in the Rust language, five widely used Rust libraries, and two vulnerability databases. In all, the investigation covered 850 unsafe code uses, 70 memory-safety defects, and 100 thread-safety defects.

In the survey, the researchers looked not only at defects reported in all vulnerability databases and publicly reported defects in software, but also at submission records in all open-source software code repositories. Through manual analysis, they identified the types of bugs they committed to fixing and grouped them into the appropriate memory safety/thread-safety issues. All the investigated issues are collated into an open Git repository: github.com/system-pclu…

Analysis of memory security problems

The study looked at 70 memory security issues. For each problem, researchers carefully analyzed the root cause and effect of the problem. The root cause of the problem is defined by modifying the patch code submitted at the time of the problem — that is, where the coding error occurred; The effect of a problem is where code runs causing observable errors, such as where a buffer overflow occurred. Because of the transmission process from root cause to effect, the two are sometimes far apart. The researchers classified errors as safe -> safe, safe -> unsafe, unsafe -> safe, and unsafe -> unsafe, depending on where the root cause and effect were in the code area. For example, if an encoding error occurs in safe code, but the effect is reflected in unsafe code, the unsafe code is categorized as safe -> unsafe.

On the other hand, the traditional classification of memory problems can be divided into Wrong Access and Lifetime Violation. It can be further subdivided into Buffer overflow, Null pointer dereferencing, Reading uninitialized memory and Invalid Free), Use after free, Double free and other small classes. According to these two classification dimensions, the statistics of the problem are as follows:

There is only one memory safety issue that does not involve the unsafe code at all. Further investigation revealed that the problem occurred in early V0.3 versions of Rust, and later stable versions of the compiler have been able to intercept the problem. The Safe code mechanism of the Rust language is very effective in avoiding memory safety issues. All memory safety issues found in stable versions are related to unsafe code.

However, this does not mean that examining all unsafe code sections is an efficient way to detect problems. Sometimes the root cause is in safe code, but the effect is in the unsafe code section. Hi3ms doesn’t have Rust code editing, so you have to make do with other languages.

Css code

pub fn sign(data: Option<&[u8]>) { let p = match data { Some(data) => BioSlice::new(data).as_ptr(), None => ptr::null_mut(), }; unsafe { let cms = cvt_p(CMS_sign(p)); }}Copy the code

In this code, p is of type RAW Pointer. In safe code, when data contains a value (Some branch), the branch attempts to create a BioSlice object and assign the object pointer to P. However, according to Rust’s lifecycle rules, the newly created BioSlice object is released at the end of the match expression, and P is a wild pointer when passed to the CMS_sign function. There is nothing wrong with the unsafe code in this example. If you only examine the unsafe code, you would not have found the error used after being freed. ** The code modified for this problem is as follows:

Css code

pub fn sign(data: Option<&[u8]>) { let bio = match data { Some(data) => Some(BioSlice::new(data)), None => None, }; let p = bio.map_or(ptr::null_mut(),|p| p.as_ptr()); unsafe { let cms = cvt_p(CMS_sign(p)); }}Copy the code

The modified code correctly extends the bio life cycle. All changes are made only to the safe code section, not to the unsafe code.

Since problems always involve unsafe code, would removing the unsafe code prevent problems? The researchers further examined all bug-fixing strategies and found that most of the changes involved unsafe code, but only a few removed it entirely. This shows that the Unsafe code cannot be completely avoided.

What’s unsafe worth? Why is it impossible to remove it completely? Looking at 600 unsafe sites, the researchers found that 42 percent were for reusing existing code (such as converting to Rust from existing C code, or calling C library functions), 22 percent were for improving performance, and the remaining 14 percent were for functionality that bypasses the Rust compiler’s checks.

Further research shows that using unsafe methods to access offset memory (such as slice::get_unchecked()) can be four to five times faster than using safe’s subscript method. This is due to Rust’s run-time validation of out-of-bounds buffers, so unsafe is essential in some performance-critical areas.

Note that the unsafe code fragment does not necessarily contain unsafe operations. The researchers found five unsafe code that even removing the unsafe tag didn’t cause any compilation errors — that is, it was perfectly safe code from the compiler’s point of view. The unsafe code is marked to alert users to key invocation contracts that cannot be checked by the compiler. A typical example is the String::from_utf8_unchecked() function in the Rust library, which doesn’t have any unsafe operations inside, but is labeled unsafe. The reason for this is that this function constructs strings directly from a user-supplied piece of memory, but does not check whether the contents are a valid UTF-8 encoding, as Rust requires that all strings be a valid UTF-8 encoding. That is, the unsafe tag for the String:: from_UTf8_unchecked () function is simply used to convey logical invocation contracts that are not directly related to memory safety, but can lead to memory safety issues elsewhere (possibly in safe code). The unsafe label is unbreakable.

Even so, eliminating unsafe code sections is an effective security improvement when possible. Looking at 130 changes to remove unsafe, the researchers found that 43 of them refactoring unsafe code into safe code, while the remaining 87 ensured security by encapsulating unsafe code into the Safe interface.

Analysis of thread safety issues

The study looked at 100 thread safety issues. The problems are divided into two categories: blocking problems (causing deadlocks) and non-blocking problems (causing data races). There are 59 blocking problems, 55 of which are related to synchronization primitives (Mutex and Condvar) :

While Rust claims to be able to program without fear of concurrency, it provides well-designed synchronization primitives to avoid concurrency problems. However, using safe code alone can lead to deadlocks caused by repeated locking, and worse, some of these problems are even specific to Rust’s design and do not occur in other languages. ** The paper gives an example:

Css code

	fn do_request() {
	    //client: Arc<RwLock<Inner>>
	    match connect(client.read().unwrap().m) {
	        Ok(_) => {
	            let mut inner = client.write().unwrap();
	            inner.m = mbrs;
	        }
	        Err(_) => {}
	    };
	}
Copy the code

In this code, the client variable is protected by an RwLock. The RwLock methods read() and write() automatically lock variables and return a LockResult object, which is unlocked automatically at the end of the LockResult object’s life cycle.

Apparently, the author of this code thought that the temporary LockResult object returned by client.read() was released and unlocked before the match branch inside the match, so it could be locked again with client.write() in the match branch. However, Rust’s lifecycle rules extend the actual life of the object returned by client.read() to the end of the match statement, so the code actually ends up trying to acquire the write() lock even before the read() lock is released, resulting in a deadlock.

This temporary object lifecycle rule is a very obscure rule in Rust and can be explained in detail in this article.

Based on proper usage of the lifecycle, the code was later modified to look like this:

Css code

	fn do_request() {
	    //client: Arc<RwLock<Inner>>
	    let result = connect(client.read().unwrap().m);
	    match result {
	        Ok(_) => {
	            let mut inner = client.write().unwrap();
	            inner.m = mbrs;
	        }
	        Err(_) => {}
	    };
	}
Copy the code

With this modification, temporary objects returned by client.read() are released at the end of the line and are not locked all the way inside the match statement.

Of the 41 non-blocking problems, 38 were due to inadequate protection of shared resources. These issues are further classified as follows, depending on how shared resources are protected and whether the code is safe or not:

Of the 38 problems, 23 occurred in the unsafe code and 15 occurred in the Safe code. Although Rust sets strict rules for borrowing and accessing data, because concurrent programming relies on the logic and semantics of the program, even SAFE code is not completely immune to data race problems. The paper gives an example:

Css code

	impl Engine for AuthorityRound {
	    fn generate_seal(&self) -> Seal {
	        if self.proposed.load() { return Seal::None; }
	        self.proposed.store(true);
	        return Seal::Regular(...);
	    }
	}
Copy the code

In this code, the proposed member of the AuthorityRound structure is a Boolean atomic variable whose value is read by load() and set by store(). Clearly, this code expects to return Seal::Regular(…) only once in a concurrent operation. Seal::None is returned. However, the manipulation of atomic variables is not handled correctly. If two threads execute to an if statement and read false results at the same time, this method may return Seal::Regular(…) to both threads. .

The code that fixes this problem is as follows, using the compare_and_swap() method to ensure that reads and writes to atomic variables are done together in a non-preemptive atomic operation.

Css code

impl Engine for AuthorityRound { fn generate_seal(&self) -> Seal { if ! self.proposed.compare_and_swap(false, true) { return Seal::Regular(...) ; } return Seal::None; }}Copy the code

The competing data issue does not involve any unsafe code; all operations are done in safe code. ** This also shows that even though Rust imposes strict concurrency checking rules, programmers still have to manually code to ensure that concurrent access is correct

Recommendations for Rust defect checking tools

Clearly, as the previous investigation shows, the Rust compiler’s checks alone are not enough to avoid all problems, and even some obscure lifecycle issues can trigger new ones. The researchers suggest adding the following inspection tools to the Rust language: 1. Improve the IDE. When a programmer selects a variable, its lifecycle range is automatically displayed, especially for objects returned by the Lock () method. This effectively solves coding problems caused by a poor understanding of the lifecycle. 2. Perform a static check on memory security. The researchers implemented a static scanning tool to check for memory security issues after release. After scanning the Rust project that participated in the study, the tool uncovered four new memory security issues that had not been previously identified. It shows that this static inspection tool is necessary. 3. Perform static check for repeated locking. The researchers implemented a static scanning tool to detect double locking by analyzing whether a variable returned by the lock() method is locked again during its lifetime. After scanning the Rust project that participated in the study, the tool found six new deadlock issues that had not been previously identified.

The paper also gives some suggestions on the application of dynamic detection and fuzzing test.

conclusion

1. The Safe code of the Rust language is very effective for checking for spatial and temporal memory safety issues, and all memory safety issues that occur in stable versions are related to the unsafe code.

2. Although the memory-safety issues all relate to the unsafe code, a number of issues also relate to the Safe code. Some problems even stem from coding errors in the Safe code rather than the unsafe code.

3. Thread-safety issues, both blocking and non-blocking, can occur in Safe code, even if the code fully conforms to the rules of the Rust language.

4. A large number of problems arise because coders do not understand the lifecycle rules of Rust correctly.

5. It is necessary to establish a new defect detection tool for typical problems in Rust language.

Click to follow, the first time to learn about Huawei cloud fresh technology ~