CS3210 Design Operating Systems

Taesoo Kim





Rust 3: Unsafety

Taesoo Kim

Administrivia

Q&A

The lab mentions that when the GPU onboard the Pi boots up, it searches for specific file names and uses them in stages to initialize and configure the CPU. How can it find and read files before even running the bootloader. Is an OS required for that? Does that mean there is some primitive OS always running on the GPU?

Q&A (cont.)

If we are going to use “unsafe Rust” to write system software, then why do we need Rust, which is claimed to be memory safer for its safety mechanisms enforced by the compiler?

Does the required use of unsafe for writing a kernel or device driver in Rust diminish the value of using Rust in the first place? If the majority of the driver’s code is inside unsafe blocks, wouldn’t we run into problems similar to what we would see trying to write safe C?

If Rust uses zero cost abstractions, why doesn’t my code compile instantly.

Summary

Configuring peripherals via MMIO

Agenda

Safety

Safety and OS development

Example: Blinky in Rust

// is it safe to invoke this function?
fn spin_sleep_ms(ms: usize) {
  for _ in 0..ms {
    // umm, invoking "nop" is unsafe?
    unsafe { asm!("nop" :::: "volatile"); }
  }
}

unsafe fn kmain() -> ! {
   // no guarantee what so ever by rustc
}

More examples

Thinking of lifetime generator

// does it make sense?
fn func() -> &T

// what about these?
fn func() -> &'static T
fn func<'a>() -> &'a T

Thinking of lifetime generator

// ref. https://doc.rust-lang.org/std/boxed/struct.Box.html#examples-8
let x = Box::new(41);
let r: &mut usize = Box::leak(x);
//     |
//     +---------> a mutable reference is returned
*r += 1;
assert_eq!(*r, 42);

Thinking of lifetime generator

// ref. https://doc.rust-lang.org/std/boxed/struct.Box.html#examples-8
let x = Box::new(41);
let r: &mut usize = Box::leak(x);
//     |
//     +---------> a mutable reference is returned
*r += 1;
assert_eq!(*r, 42);

impl<T> Box<T> {
  pub fn leak<'a>(b: Box<T>) -> &'a mut T { ... }
}

Things considered (surprisingly) safe in Rust

  1. Leak memory → wasted memory, but no dangled pointer
let x = Box::new(41);
let r: &mut usize = Box::leak(x);
*r += 1;
assert_eq!(*r, 42);

Things considered (surprisingly) safe in Rust

  1. Leak memory → wasted memory, but no dangled pointer
  2. Overflow integers → overflowed, but no memory-related violation after
  3. Acquiring raw pointers → safe as far as not dereferenced
  4. Deadlock → hanging, but not crashing
  5. Have a race condition → inconsistent state, but no memory corruption

→ Think about why they can be considered safe? (yet undesirable)

panic vs. crashing

→ In OS, both are equally bad; don’t abuse panic or its variants like unwrap

Example: leaking memory

let s1 = String::from("Hello world!");
std::mem::forget(s1);
//=> no reclaiming of the memory as it own't call drop()

let s2 = Box::new(0xdeadbeef as u64);
Box::leak(s2);
//=> ditto

Example: acquiring raw pointers

let x = 1;
let y = &x as *const i32;
//         |
//         +-> it coerces a reference type to a raw pointer

Example: dereferencing raw pointers

let x = 1;
let y = &x as *const i32;
//         |
//         +-> it coerces a reference type to a raw pointer

unsafe {
  assert_eq!(y.read_volatile(), 1);
  //                    |
  //                    +-> intrinsics::volatile_load(y)
}

Example: dereferencing raw pointers

let mut x = 1;
let y = &mut x as *mut i32;

unsafe {
  y.write_volatile(2);
}

assert_eq!(x, 2);

Example: going widely with raw pointers

let y = 0xdeadbeef as *const i32;

unsafe {
  y.read_volatile();
  //=> Program received signal SIGSEGV (fault address 0xdeadbeef)
}

Thinking of accessing raw pointers

→ What are the legitimate use cases in applications?

CS101: Integer Representation

Ref. https://en.wikipedia.org/wiki/Integer_overflow

CS101: Two’s Complement Representation

e.g., in x86 (32-bit, 4-byte):
    - 0x00000000 ->  0
    - 0xffffffff -> -1
    - 0x7fffffff ->  2147483647 (INT_MAX)
    - 0x80000000 -> -2147483648 (INT_MIN)

Ref. https://en.wikipedia.org/wiki/Two's_complement

Arithmetic with Two’s Complements

    0x00000001 + 0x00000002 = 0x00000003 ( 1 + 2 = 3)
    0xffffffff + 0x00000002 = 0x00000001 (-1 + 2 = 1)
    0xffffffff + 0xfffffffe = 0xfffffffd (-1 +-2 =-3)

    range(signe integer) = [-2^31, 2^31-1] = [-2147483648, 2147483647]
    range(unsigned integer) = [0, 2^32-1] = [0, 4294967295]

Integer overflows in Rust (RFC 560)

Implication of the specified behavior

let x: i64 = ...;

if (x < 0) {
  // is this a true statement?
  if (x < -x) {
    println!("1");
  } else {
    // is this reachable? can be optimized away?
    println!("2");
  }
} else {
    println!("3");
}

Handling integer overflows

// a + b:
//  in debug  : a.checked_add(b).unwrap()
//  in release: a.wrapping_add(b)

fn overflowing_add(self, rhs: i64) -> (i64, bool);
fn wrapping_add(self, rhs: i64) -> i64;
fn saturating_abs(self) -> i64;

fn checked_add(self, rhs: i64) -> Option<i64>;

Inline assembly

#![feature(asm)]

fn add(x: u64, y: u64) -> u64 {
    let rtn;
    unsafe {
        asm!("add $0, $2"
             : "=r"(rtn)
             : "0"(x), "r"(y)
             :
             : "intel");
    }
    return rtn
}

dbg!(add(1, 2));

Ref. Inline Assembly

Example: including assembly code

// src/init.rs
global_asm!(include_str!("init/init.s"));

#[no_mangle]
unsafe fn kinit() -> ! {
    zeros_bss();
    kmain();
}

Static: mutable global variable

static mut GLOBAL: i64 = 0;

fn main() {
    unsafe {
        dbg!(GLOBAL);
    }
}

Union (like C’s)

union Buf {
  int: u32,         // either u32
  buf: [u8; 4],     // or a byte array
}

fn main() {
  let u = Buf {
    int: 0xdeadbeef,
  };

  unsafe {
    dbg!(u.int);
    dbg!(u.buf);
  }
}

Example: union on Atag (in AArch64)

#[repr(C)]
pub struct Atag {
  pub dwords: u32,
  pub tag: u32,    // TAG indicates the type of 'Kind'
  pub kind: Kind
}

#[repr(C)]
pub union Kind {
  pub core: Core,
  pub mem: Mem,
  pub cmd: Cmd
}

FFI

extern crate libc;

use std::ffi::CString;
use libc::{size_t, c_char, c_void, c_int};

extern {
  fn malloc(size: size_t) -> *mut c_void;
  fn printf(format: *const c_char, ...) -> c_int;
}

fn main() {
  unsafe { malloc(100); }

  let fmt = CString::new("hello: %d\n").unwrap();
  unsafe {
    printf(fmt.as_ptr(), 1);
  }
}

Transmute

let s1 = "Hello world!";
let s2 = unsafe {
  std::mem::transmute::<&str, (u64, u64)>(s1)
};

dbg!(s1); //=> "Hello world!"
dbg!(s2); //=> (ptr-to-hello-world, 12)

Example: (widely dangerous) transmute

let s1 = String::from("Hello world!");
let s2 = unsafe {
  // s1 moves, and no drop() will be called in transmute()
  std::mem::transmute::<String, (u64, u64, u64)>(s1)
};
dbg!(s2);

unsafe {
  // not dangled
  printf(s2.0 as *const c_char);
}

TCB: Rustc and all correctness of unsafe blocks

Example: incorrect behavior of Rustc

// rustc-1.35(stable)
fn tst_func(bar: String) -> bool {
  drop(bar);
  false
}

fn main() {
  let bar = String::new("BAR");
  let bar = match 0 {
    0 if { tst_func(bar) } => unreachable!(),
    _ => { bar },
  };
  println!("{:#?}", bar);
}

Ref. Evading Rust’s memory safety

Example: incorrect unsafe code in stdlib

Example: incorrect unsafe code in stdlib

pub fn reserve(&mut self, additional: usize) {
  let old_cap = self.cap();
  let used_cap = self.len() + 1;
  let new_cap = used_cap.checked_add(additional)
        .and_then(|needed_cap| needed_cap.checked_next_power_of_two())
        .expect("capacity overflow");

  if new_cap > self.capacity() {
    self.buf.reserve_exact(used_cap, new_cap - used_cap);
    unsafe {
      self.handle_cap_increase(old_cap);
    }
  }
}

Ref. OOB in VecDeque::reserve(), CVE-2018-1000657.

Example: incorrect unsafe code in stdlib

Ref. Fix capacity comparison in reserve #44802

Toward development of memory-safe OS

Next lecture

References