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

Reading on uninitialized memory may cause UB ( util::read_spv() ) #354

Closed
JOE1994 opened this issue Jan 7, 2021 · 8 comments · Fixed by #470
Closed

Reading on uninitialized memory may cause UB ( util::read_spv() ) #354

JOE1994 opened this issue Jan 7, 2021 · 8 comments · Fixed by #470
Labels

Comments

@JOE1994
Copy link

JOE1994 commented Jan 7, 2021

Hello 🦀 ,
we (Rust group @sslab-gatech) found a memory-safety/soundness issue in this crate while scanning Rust code on crates.io for potential vulnerabilities.

Issue Description

https://github.com/MaikKlein/ash/blob/7fa182cc4330da8da98a7c023049162a4979d0bf/ash/src/util.rs#L116-L124
util::read_spv() method creates an uninitialized buffer and passes it to user-provided Read implementation. This is unsound, because it allows safe Rust code to exhibit an undefined behavior (read from uninitialized memory).

This part from the Read trait documentation explains the issue:

It is your responsibility to make sure that buf is initialized before calling read. Calling read with an uninitialized buf (of the kind one obtains via MaybeUninit<T>) is not safe, and can lead to undefined behavior.

Suggested Fix

It is safe to zero-initialize the newly allocated u32 buffer before read(), in order to prevent user-provided Read from accessing old contents of the newly allocated heap memory.

Also, there are two nightly features for handling such cases.

Thank you for checking out this issue 👍

@MaikKlein MaikKlein added the bug label Jan 8, 2021
@JOE1994
Copy link
Author

JOE1994 commented Jan 25, 2021

Unfortunately the safe yet naive fix of zero-initializing the newly allocated buffer incurs runtime overhead 😢
As of Jan 2021, there doesn't seem to be an ideal fix that works in stable Rust with no performance overhead.
Below are links to relevant discussions & suggestions for the fix.

@MarijnS95
Copy link
Collaborator

Yikes, it looks like rustsec/advisory-db#680 managed to get merged just yesterday (after sitting for half a year...).

I'm inclined to just remove this function altogether or ban it away to a separately-published util crate, it has nothing to do with Ash's immediate goal of wrapping Vulkan. For parsing SPIR-V one can always resort to rspirv.

MarijnS95 added a commit that referenced this issue Aug 23, 2021
Fixes #354

`io::Read::read_exact` does not receive `MaybeUninit` memory and a trait
implementation can possibly read from our uninitialized vector without
`unsafe`, which is UB.  As there is no proper solution to this problem
yet (see linked issue), our safest bet is to just take the perf-hit and
zero-initialize this vector.
MarijnS95 added a commit that referenced this issue Aug 23, 2021
Fixes #354

`io::Read::read_exact` does not receive `MaybeUninit` memory and a trait
implementation can possibly read from our uninitialized vector without
`unsafe`, which is UB.  As there is no proper solution to this problem
yet (see linked issue), our safest bet is to just take the perf-hit and
zero-initialize this vector.
MaikKlein pushed a commit that referenced this issue Aug 23, 2021
…470)

Fixes #354

`io::Read::read_exact` does not receive `MaybeUninit` memory and a trait
implementation can possibly read from our uninitialized vector without
`unsafe`, which is UB.  As there is no proper solution to this problem
yet (see linked issue), our safest bet is to just take the perf-hit and
zero-initialize this vector.
MarijnS95 added a commit to MarijnS95/advisory-db that referenced this issue Aug 23, 2021
Shnatsel pushed a commit to rustsec/advisory-db that referenced this issue Aug 23, 2021
@Ralith
Copy link
Collaborator

Ralith commented Aug 23, 2021

I'm inclined to just remove this function altogether or ban it away to a separately-published util crate, it has nothing to do with Ash's immediate goal of wrapping Vulkan. For parsing SPIR-V one can always resort to rspirv.

read_spv is for loading SPIR-V, not parsing it. We should keep it because ~every single Vulkan application needs to do this, and it's surprisingly tricky to get right (as illustrated by this issue).

@MarijnS95
Copy link
Collaborator

I've misread the function initially; we always use a compiler that emits SPIR-V in host-endianness (what vkCreateShaderModule expects) and have never had to use this function.

@Ralith
Copy link
Collaborator

Ralith commented Aug 23, 2021

Handling endianness isn't actually the main benefit, though it's nice. Vulkan requires that SPIR-V be passed in as a valid u32 pointer, which means that it must be properly aligned. The naive approach of reading into a Vec<u8> or employing include_bytes does not guarantee this, and is hence unsound.

we always use a compiler that emits SPIR-V in host-endianness

Note that what you actually want here is target endianness, though this only differs when cross compiling.

@MarijnS95
Copy link
Collaborator

MarijnS95 commented Aug 25, 2021

Handling endianness isn't actually the main benefit, though it's nice. Vulkan requires that SPIR-V be passed in as a valid u32 pointer, which means that it must be properly aligned.

This is fairly trivial too, not "surprisingly tricky to get right" - this function could have been safe if it didn't juggle endianness around.

Note that what you actually want here is target endianness, though this only differs when cross compiling.

I was referring to the "host" that runs our Vulkan program, fair to call that a target. We compile the few shaders that we have on that target machine at runtime, so it's always the same on our end. Whatever, we're bikeshedding now 😬

@Ralith
Copy link
Collaborator

Ralith commented Aug 25, 2021

This is fairly trivial too, not "surprisingly tricky to get right"

The number of times I've had to explain to people that it's necessary at all (see e.g. #245) begs to differ, at least :P

this function could have been safe if it didn't juggle endianness around.

The endianness flip is already 100% safe. The unsafety is required to treat an array of u32s as an array of u8s for I/O purposes. This could be made safe instead at the cost of additional copies, which might discourage its use in favor of unsound pointer casting.

@MarijnS95
Copy link
Collaborator

Implicitly saying that, if we took the penalty for copies, we can still change endianness (in a 100% safe way) but we wouldn't be here discussing this in the first place. The flip itself isn't the problem, the way it is implemented is (or was).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging a pull request may close this issue.

4 participants