Author: Zhang Handong

This series focuses on analyzing security issues found in the Rust ecosystem community recorded in the RustSecurity security database, and summarizing some of the lessons learned from Rust security programming.

This issue analyzes the following six security issues:

  • Rustsec-2021-0067: Code generation defect in Cranelift module resulting in possible WASM sandbox escape
  • Rustsec-2021-0054: RKYV Crate may contain uninitialized memory
  • Rustsec-2021-0041: parse_duration rejects service (DOS) by parsing the Payload with too large an index
  • Rustsec-2021-0053: In the algorithm librarymerge_sort::merge()Type double-free that causes Drop to be implemented
  • Rustsec-2021-0068: Soundness issue in the ICED x86 version
  • Rustsec-2021-0037: Diesel library Sqlite UAF(use-after-free) bug

See if it gives us some insight.

Rustsec-2021-0067: Code generation defect in Cranelift module resulting in possible WASM sandbox escape

A loophole was found in Cranelift. Operations with unknown inputs cause privilege escalation vulnerabilities. CWe is classifying the problem as CWE-264. This has implications for confidentiality, integrity and availability.

Vulnerability description:

  • Vulnerability type: Vulnerability
  • Code-execution/memory-corruption/ memory-exposure
  • CVE number: CVE-2021-32629
  • Details: github.com/bytecodeall…
  • Architecture: x86
  • Patch:> = 0.73.1> = 0.74.0

There is a bug in 0.73.0 on the Cranelift X64 backend that creates a scenario that could lead to a potential sandbox escape in the Webassembly module. Users of Version 0.73.0 of Cranelift should upgrade to 0.73.1 or 0.74 to fix this vulnerability.

If the old default backend is not used, Cranelift users prior to 0.73.0 should be updated to 0.73.1 or 0.74.

Vulnerability analysis

This problem was introduced in the new back end of Cranelift (Cranelift underwent too much refactoring).

Some background: register allocation

If the number of physical registers is insufficient to meet the requirements of virtual registers, some virtual registers will obviously only map to memory. These virtual registers are called spill virtual registers. The register allocation algorithm directly determines the register utilization rate in the program.

Cranelift register allocation cfallin.org/blog/2021/0…

The article also details how the team ensured that Cranelift generated the correct code. Even so, there are logic bugs.

This Bug is a logic Bug:

The reason is that when the register allocator reloads spill integer values that are narrower than 64-bit, the value loaded from the stack performs sign expansion instead of zero expansion.

This has bad implications for another optimization: the instruction selector will select a 32-bit to 64-bit zero-extension operator when we know that the instruction producing a 32-bit value will actually zero the high 32 of its target register. So we rely on these zeroing bits, but the value is still of type I32, and overflow/reload the symbolic extension of MSB that restructures these bits to I32.

So, in certain cases, if the i32 value is a pointer, sandbox escape may occur. The regular code emitted for heap access zero-extends the WebAssembly heap address, adds it to the 64-bit heap base, and then accesses the resulting address. If the zero extension becomes a sign extension, the module can go backwards and access up to 2GiB of memory before the heap starts.

Sign-extend: The operation of increasing the number of binary digits while preserving the symbols and values of the digits.

Zero-extend: Used to move unsigned numbers into larger fields while preserving their values.

The impact of the Bug depends on the implementation of the heap. Specifically:

If the heap has boundary checking. Also, it is not entirely dependent on securing pages. And the heap binding is 2GiB or less. The Bug cannot be used to access memory from another WebAssembly module heap.

This vulnerability can be mitigated if there is no mapped memory in the scope accessible using this Bug, for example, if the WebAssembly module heap is preceded by a 2 GiB protected area.

  • Fix: PR github.com/bytecodeall…
  • Click here to see more about the impact on Lucet and Wastmtime: github.com/bytecodeall…

Rustsec-2021-0054: RKYV Crate may contain uninitialized memory

Vulnerability description:

  • Vulnerability type: Vulnerability
  • Memory-exposure
  • CVE id: None
  • Details: github.com/djkoloski/r…
  • Patch:> = 0.6.0

Rkyv is a serialization framework during serialization, it may not be possible to initialize structure fill bytes and unused enumeration bytes. These bytes can be written to disk or sent over insecure channels.

Vulnerability analysis

Patch code: github.com/djkoloski/r…

Problematic code:

unsafe fn resolve_aligned<T: Archive + ? Sized>( &mut self, value: &T, resolver: T::Resolver, ) -> Result<usize, Self::Error> { // ... let mut resolved = mem::MaybeUninit::zeroed(); / /... }Copy the code

The mem::MaybeUninit:: Zeroed () function creates a new instance of MaybeUninit

and the memory bit is filled with zeros. But that depends on T being initialized correctly. For example: MaybeUninit

::zeroed() is initialized, but MaybeUninit<&’static i32>::zeroed() is not initialized correctly. This is because references in Rust cannot be empty.

Therefore, the resolver is a generic T and may not be initialized correctly, so there is a risk that it will not be initialized.

Fixed code:

    let mut resolved = mem::MaybeUninit::<T::Archived>::uninit();
    resolved.as_mut_ptr().write_bytes(0, 1);
Copy the code

Just assume it is not properly initialized, and then manually initialize it using write_bytes to make sure it is correct.

Rustsec-2021-0041: parse_duration rejects service (DOS) by parsing the Payload with too large an index

Vulnerability description:

  • Vulnerability type: Vulnerability
  • Category vulnerability: denial-of-service
  • CVE Number: CAN-2021-1000007 / CVE-2021-29932
  • Details: github.com/zeta12ti/pa…
  • Patch: None, the author gives up maintenance

Vulnerability analytical

The parse_duration library is used to parse a string into a duration.

Problem code:

if exp < 0 {
    boosted_int /= pow(BigInt::from(10), exp.wrapping_abs() as usize);
} else {
    boosted_int *= pow(BigInt::from(10), exp.wrapping_abs() as usize);
}
duration.nanoseconds += boosted_int;
Copy the code

This is a code snippet within the parse function that allows exponential duration string parsing, where the BigInt type is used for this type of Payload along with the POW function. This function will occupy CPU and memory for a long time.

This allows an attacker to use the Parse feature to create a DOS attack. Although the library is no longer maintained and the number of stars is low, it is not clear how many libraries depend on it. Use cargo Audit to check dependencies in your project.

Rustsec-2021-0053: In the algorithm librarymerge_sort::merge()Type double-free that causes Drop to be implemented

  • Vulnerability type: Vulnerability
  • Vulnerability classification: memory-corruption
  • CVE id: None
  • Details: github.com/AbrarNitk/a…
  • Patch: None yet

Vulnerability analysis

Algorithmica is Rust algorithm teaching library, website is: www.fifthtry.com/abrar/rust-…

In the implementation of merge sort in this library, the merge function results in double ownership of the list elements, so there is a double free.

The unsafe rust implementation appears in the following source code.

fn merge<T: Debug, F>(list: &mut [T], start: usize, mid: usize, end: usize, compare: &F) where F: Fn(&T, &T) -> bool, { let mut left = Vec::with_capacity(mid - start + 1); let mut right = Vec::with_capacity(end - mid); unsafe { let mut start = start; while start <= mid { left.push(get_by_index(list, start as isize).read()); start += 1; } while start <= end { right.push(get_by_index(list, start as isize).read()); start += 1; } } let mut left_index = 0; let mut right_index = 0; let mut k = start; unsafe { while left_index < left.len() && right_index < right.len() { if compare(&left[left_index], List [k] = get_by_index(&left, left_index as isize).read(); left_index += 1; } else { list[k] = get_by_index(&right, right_index as isize).read(); right_index += 1; } k += 1; } while left_index < left.len() { list[k] = get_by_index(&left, left_index as isize).read(); left_index += 1; k += 1; } while right_index < right.len() { list[k] = get_by_index(&right, right_index as isize).read(); right_index += 1; k += 1; } } } unsafe fn get_by_index<T>(list: &[T], index: isize) -> *const T { let list_offset = list.as_ptr(); list_offset.offset(index) }Copy the code

Bug repetition:

#! [forbid(unsafe_code)] use algorithmica::sort::merge_sort::sort; fn main() { let mut arr = vec! [ String::from("Hello"), String::from("World"), String::from("Rust"), ]; // Calling `merge_sort::sort` on an array of `T: Drop` triggers double drop algorithmica::sort::merge_sort::sort(&mut arr); dbg! (arr); }Copy the code

Output:

free(): double free detected in tcache 2

Terminated with signal 6 (SIGABRT)
Copy the code

The Bug has not been fixed.

The enlightenment of this problem to us: do not ignore security in order to brush the question.

Rustsec-2021-0068: Soundness issue in the ICED x86 version

Vulnerability description:

  • Vulnerability type: Vulnerability
  • Soundness
  • CVE id: None
  • Details: github.com/icedland/ic…
  • Patch:> 1.10.3

Vulnerability analysis

Iced users compile their projects using miri and find UB:

error: Undefined Behavior: memory access failed: pointer must be in-bounds at offset 4, but is outside bounds of alloc90797 which has size 3 --> C:\Users\lander\.rustup\toolchains\nightly-x86_64-pc-windows-msvc\lib\rustlib\src\rust\library\core\src\slice\mod.rs:365 :18 | 365 | unsafe { &*index.get_unchecked(self) } | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ memory access failed: pointer must be in-bounds at offset 4, but is outside bounds of alloc90797 which has size 3 | = help: this indicates a bug in the program: it performed an invalid operation, and caused Undefined Behavior = help: see https://doc.rust-lang.org/nightly/reference/behavior-considered-undefined.html for further information = note: inside `core::slice::<impl [u8]>::get_unchecked::<usize>` at C:\Users\lander\.rustup\toolchains\nightly-x86_64-pc-windows-msvc\lib\rustlib\src\rust\library\core\src\slice\mod.rs:365 :18 = note: inside `iced_x86::Decoder::new` at Ecc6299db9ec823 \ iced C:\Users\lander\.cargo\registry\src\github.com - 1 - x86-1.9.1 \ SRC \ decoder \ mod rs: 457:42 note: inside `Emulator::run` at src\lib.rs:563:27 --> src\lib.rs:563:27 | 563 | let mut decoder = Decoder::new(self.bitness, bytes, self.decoder_options);Copy the code

UB appeared when using Decoder:: New. In the source code of iced, namely, iced/ SRC /rust/iced-x86/ SRC /decoder.rs, exist

let data_ptr_end: *const u8 = unsafe { 
    data.get_unchecked(data.len()) 
};
Copy the code

According to the standard library documentation:

Calling this method with an out-of-bounds index is undefined behavior even if the resulting reference is not used.

Calling this method with an out-of-bounds index is undefined behavior (UB), even if the reference to the result is not used.

Example:

let x = &[1, 2, 4]; unsafe { assert_eq! (x.get_unchecked(1), &2); assert_eq! (x.get_unchecked(3), &2); // UB}Copy the code

This code has been fixed to no longer use get_unchecked:

let data_ptr_end = data.as_ptr() as usize + data.len();
Copy the code

Rustsec-2021-0037: Diesel library Sqlite UAF(use-after-free) bug

Vulnerability description:

  • Vulnerability type: Vulnerability
  • Vulnerability classification: memory-corruption
  • CVE number: CVE-2021-28305
  • Details: github.com/diesel-rs/d…
  • Patch:> = 1.4.6

Vulnerability analysis

Diesel’s SQLite backend uses the libsqlite3_sys library to call SQL functions provided by SQLite. Such as SQlite3_Finalize and SQlite3_step.

The sqLite function performs the call:

  • sqlite3_open()

  • sqlite3_prepare()

  • Sqlite3_step () // Used to execute the precompiled statement created by sqlite3_prepare earlier

  • Sqlite3_column () // Returns a column in the current row of the result set from executing a precompiled statement sqlite3_step()

  • Sqlite3_finalize () // Destroys the precompiled statement created earlier by sqlite3_prepare

  • sqlite3_close()

Diesel’s common practice for by_name queries is to save all field names of precompiled statements as string slices for later use.

But sqLite behaves like this:

  • The string pointer returned is valid until the prepared statement is removedsqlite3_finalize()Destruction,
  • Or until the first callsqlite3_step()Automatically reprecompile the statement for a particular run,
  • Or until the next callsqlite3_column_name()sqlite3_column_name16()In the same column.

In previous versions of Diesel, this was not noticed, and the string slice pointer was invalidated after sqlite3_step() was reprecompiled. That creates a UAF situation.

The lesson of this example is to be aware of the behavior associated with binding sys libraries when using FFi. This is not detected by the Rust compiler and should be a logical Bug.

In the previous

Recommendation engine project | framework

The next article

Rust and security | Rust make malicious software is better