Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Potential unsoundnesses (not yet determined) with use of unsafe #37

Open
Manishearth opened this issue Apr 1, 2024 · 0 comments
Open

Comments

@Manishearth
Copy link

Hi!

In the process of unsafe-reviewing this crate, we discovered some issues. It's as of yet unclear if there is any unsoundness here, however the invariants are sufficiently nonlocal that it warrants further review (which I may not have time for at the moment)

Most of these are of the form "unsafe precondition checked by debug assertion; and may be exposed to the user through many layers of API"

The first one is this code:

debug_assert!(q >= SMALLEST_POWER_OF_FIVE as i64);
debug_assert!(q <= LARGEST_POWER_OF_FIVE as i64);
debug_assert!(precision <= 64);
let mask = if precision < 64 {
0xFFFF_FFFF_FFFF_FFFF_u64 >> precision
} else {
0xFFFF_FFFF_FFFF_FFFF_u64
};
let index = (q - SMALLEST_POWER_OF_FIVE as i64) as usize;
let (lo5, hi5) = unsafe { *POWER_OF_FIVE_128.get_unchecked(index) };

The safety invariants are upheld by a debug assertion, but it's not guarded in release mode. Ultimately, the q value comes from a parsed exponent, which seems quite sketchy: you really do not want safety invariants relying on outputs of your parsers: indeed: this is one of the most common ways for there to be exploitable undefined behavior in C code.

pub fn step_by(&mut self, n: usize) -> &mut Self {
unsafe { self.ptr = self.ptr.add(n) };

This doesn't do any bounds checking. It's possible the many uses of step_by() and step() in this codebase are correct, but it will take time to verify.

There are a couple ways to use typestate to write munchers that can avoid duplicating bounds checks here so that the code can be written with the same performance. Happy to talk about that in more depth.

unsafe { *self.ptr }

This also doesn't check invariants. Unclear if upheld at the call site.

fn read_u64(&self) -> u64 {
debug_assert!(self.as_ref().len() >= 8);
let src = self.as_ref().as_ptr() as *const u64;
u64::from_le(unsafe { ptr::read_unaligned(src) })
}
#[inline]
fn write_u64(&mut self, value: u64) {
debug_assert!(self.as_ref().len() >= 8);
let dst = self.as_mut().as_mut_ptr() as *mut u64;
unsafe { ptr::write_unaligned(dst, u64::to_le(value)) };
}

Only checks invariants in debug mode. Unclear if upheld at the call sites.

Alexhuszagh added a commit to Alexhuszagh/fast-float-rust that referenced this issue Oct 31, 2024
This patches a lot of the wrappers in `AsciiStr` being marked as safe but not being safe except within the context, using raw pointer dereferences without local bounds checks.

This is extensively documented in aldanor#37:
    aldanor#37

`AsciiStr` has been re-written as a result, and unsafe functions marked as safe have been either converted to safe variants where the compiled checks can be ellided or marked as unsafe so the caller knows to upholds the safety invariants.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant