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

vulkan: Mark fn mapped_(mut_)slice() as unsafe #139

Open
wants to merge 1 commit into
base: main
Choose a base branch
from

Conversation

MarijnS95
Copy link
Member

CC @fu5ha

As discussed long ago, and recently in #138, it is undefined behaviour to create or transmute to &[u8] when the underlying data is possibly uninit. This also holds true for transmuting arbitrary T: Copy structures to &[u8] where eventual padding bytes are considered uninitialized, hence invalid for u8.

Instead of coming up with a massive safety API that distinguishes between uninitialized and initialized buffers - which turn out to be really easy to invalidate by copying structures with padding bytes - place the onus on the user to keep track of initialization status by only ever providing mapped slices in an unsafe context. Users are expected to initialize the buffer using ptr::copy(_nonoverlapping)() when used from a CPU context instead of calling .mapped_mut_slice(), or switch to the new presser API from #138.

Comment on lines +107 to +110
/// Only to be called when the memory is known to be _fully_ initialized. Use [`std::ptr::copy()`] or
/// [`std::ptr::copy_nonoverlapping()`] on [`mapped_ptr()`][Self::mapped_ptr()] to initialize this buffer
/// from the CPU instead.
pub unsafe fn mapped_mut_slice(&mut self) -> Option<&mut [u8]> {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of always requiring pointers for CPU-side initialization, we can also provide a safe fn that returns &(mut) [MaybeUninit<T>], allowing the caller to still initialize from slices more easily, without pulling in the whole (borked) "initialization tracking" from https://github.com/Traverse-Research/gpu-allocator/compare/uninit.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 yeah, though if we wanted it to be actually safe it would still need to be &mut [MaybeUninit<u8>], or check alignment and size requirements for T within (also doable)

Comment on lines +107 to +110
/// Only to be called when the memory is known to be _fully_ initialized. Use [`std::ptr::copy()`] or
/// [`std::ptr::copy_nonoverlapping()`] on [`mapped_ptr()`][Self::mapped_ptr()] to initialize this buffer
/// from the CPU instead.
pub unsafe fn mapped_mut_slice(&mut self) -> Option<&mut [u8]> {
Copy link
Member Author

@MarijnS95 MarijnS95 Oct 18, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note that I sneaked a little mapped_slice_mut -> mapped_mut_slice renime in here, to better match as_slice/as_mut_slice in Rust std.

@fu5ha fu5ha mentioned this pull request Oct 18, 2022
@fu5ha
Copy link
Contributor

fu5ha commented Oct 18, 2022

I think that this is likely a good way forward. It might be the best to just not expose mapped_slice at all and only ever expose the raw base pointer, though exposing it as a &[u8] under an unsafe fn and making it clear the guarantees that implies is also fine. I've also thought about adding support for some unsafe functions to presser for casting some subsection of a Slab into a &[T] for reading. These would need to be unsafe but could help avoid some common pitfalls and list all the actual needed safety invariants in an easy-to-see place on them, but that's not a reality now so having these is probably fine.

@MarijnS95
Copy link
Member Author

MarijnS95 commented Oct 18, 2022

It seems useful to me to have these functions available - with the right # Safety context - to hopefully prevent users from implementing it themselves without being aware of these problems.

That said, @fu5ha I assume for this to really be useful we should replace u8 with T so that a struct with padding can still be accessed safely, as &[T] is safe to access whereas &[u8] is not.

What are your thoughts on also adding a &mut [MaybeUninit<T>] API (possibly both mut and non-mut variants)?

@fu5ha
Copy link
Contributor

fu5ha commented Oct 18, 2022

I'm a fan. I think the list of apis could be something like

/// `offset` is in bytes from start of allocation, `len` is the number of `T`s in the returned slice
unsafe fn mapped[_mut]slice_from_offset<T>(&[mut] self, offset: usize, len: usize) -> Option(&[mut] [T]) {}

/// Same as above but doesn't validate alignment or size requirements for you
unsafe fn mapped[_mut]_slice_from_offset_unchecked<T>(&[mut] self, offset: usize, len: usize) -> Option(&[mut] [T]) {}

/// Note this can be safe because `MaybeUninit` ships the unsafety to `assume_init` call.
/// `offset` is in bytes from start of allocation, `len` is the number of `T`s in the returned slice
fn mapped_maybe_uninit[_mut]_slice_from_offset<T>(&[mut] self, offset: usize, len: usize) -> Option(&[mut] [MaybeUninit<T>]) {}

/// Same as above but doesn't validate alignment or size requirements for you so unsafe again
unsafe fn mapped_maybe_uninit[_mut]_slice_from_offset_unchecked<T>(&[mut] self, offset: usize, len: usize) -> Option(&[mut] [MaybeUninit<T>]) {}

Also arguably the non-mut version of the MaybeUninit api is pretty much useless because at least as of the current stable MaybeUninit interface you can't actually do anything with it and if you're just going to assume_init immediately you may as well just use the unsafe version that returns an actual slice.

Notably these are also the set of apis (plus ones that just return a bare &[mut] T rather than a slice) that I'd want to implement for this purpose in presser, so this could be a good excuse to do that and then just ship them thru to it in the gpu-allocator functions.

})
///
/// # Safety
/// Only to be called when the memory is known to be _fully_ initialized.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As a user of gpu_allocator, I can only get an Allocation using Allocator::allocate. The memory is at that point fully initialized because any bit pattern is a valid byte. Freeing likewise takes my instance, preventing unsafe usage.

Does this need to be unsafe?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The memory is at that point fully initialized because any bit pattern is a valid byte

According to https://crates.io/crates/presser / https://www.ralfj.de/blog/2019/07/14/uninit.html, and per the description/trigger of this PR, that is not the case.

Copy link
Contributor

@fu5ha fu5ha Oct 18, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The memory is at that point fully initialized because any bit pattern is a valid byte.

But this does not logically follow. Memory initialization is not about whether some bit pattern is a valid byte or not, it's a specific meta-state change about a piece of memory that happens the first time data is written into it after being marked uninit. Memory returned from Allocator::allocate is not initialized. uninit memory is not just "any bit pattern", it is a completely separate "meta-state" about a piece of memory that means it could not just be any bit pattern but it could very well not exist at all, and thus the compiler is free to make optimizations, reorder your code, etc. based on this assumption. reading an uninit byte is always ub, even if your code is valid for any bit pattern -- see readme here https://github.com/EmbarkStudios/presser

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, I see, the unsafety is reading possibly uninitialized data.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@attackgoat more concretely, will a MaybeUninit<T> API work for your use-case?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see the danger in using data which has "random" values, especially when transmuting to T; but I'm not entirely convinced that this is unsafe in the context. It's more unwise. The GPU is very likely to do things to our memory as we hold it; so additional safeguards don't seem fool proof.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Whether the GPU is doing things or not is a wholly separate issue, though it does make things another level of unsafe. In that regard, most of the rust graphics libraries have taken the stance that the GPU overlapping access with CPU access doesn't constitute marking a function as unsafe because validating that at compile time is essentially impossible and classifying it as unsafe would mean pretty much every gpu api ever would be unsafe which just doesn't seem like it adds any value. So instead we reserve unsafe to demarcate when a function is extra unsafe and should be looked at even more carefully than just for synchronization with GPU access. Once again though, uninit =/= "random", in fact, if the GPU is writing data with uninit/padding and we are reading it back as &[u8] that's actually unsound whereas reading it back as &[T] which has the same layout as what the GPU wrote is completely sound and valid.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I prefer a strong # Safety warning that states the danger involved, and at least an unsafe marker. I don't know if I'm knowledgeable enough to know about MaybeUnint exactly.

Copy link
Contributor

@fu5ha fu5ha Oct 18, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The goal is to have an api that makes things almost-as-ergonomic (or even more ergonomic in many cases) while avoiding the most common safety pitfalls and calling out more explicitly the ways things can go wrong thru stronger safety warnings like you say :)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes and I'll add that I like the idea of #138 because Screen 13 has the same old pattern using bytemuck and it would be nice to offer users a safer way of doing this.

@MarijnS95
Copy link
Member Author

@fu5ha Good point on the offset and len!

Not sure if we should:

  1. Merge this PR as a stop-gap solution;
  2. With or without MaybeUninit;
  3. With or without u8 -> T, requiring an offset and size (otherwise the user needs to go through &[u8] anyway, offset, and from_raw_parts);
  4. Wait for you to implement this in presser, and replace the entire public mapped_slice[_mut]() API with it at once.

@MarijnS95 MarijnS95 changed the title vulkan: Mark fn mapped_(mut_)slice() as `unsafe vulkan: Mark fn mapped_(mut_)slice() as unsafe Oct 18, 2022
@fu5ha
Copy link
Contributor

fu5ha commented Oct 18, 2022

My take is probably that things are "fine" just as is for now, people are unlikely to change to new apis between now and the near-ish future where we could replace the whole api as discussed. Especially since I don't think it'll take too long to implement those in presser and then add the APIs here on top. Plus that will be one less breaking change for users to have to update to.

@fu5ha
Copy link
Contributor

fu5ha commented Oct 18, 2022

Opened EmbarkStudios/presser#5 which implements basically the idea I discussed above, atop which it should be fairly easy to implement similar APIs in gpu-allocator, though probably not all of them are needed to be included directly, since we can just go through the Slab impl proposed in #138 for the rest.

@MarijnS95
Copy link
Member Author

MarijnS95 commented Oct 19, 2022

@fu5ha Sure! This PR only took a few minutes to rig up, I'm fine with closing it and migrating the whole public mapping API to presser as a whole.

We could keep the (default enabled) presser feature in #138, and just not provide any mapping API at all if it is disabled - or commit the changes provided by this PR, including the modifications (size+offset, and a safe MaybeUninit accessor) suggested above.

@fu5ha
Copy link
Contributor

fu5ha commented Oct 19, 2022

Whatever you think.

I'm a bit biased but I think doing this would be a good order:

  1. Keep how it is for now
  2. land Add support for presser #138
  3. land Add read helpers EmbarkStudios/presser#5 and release presser 0.4
  4. update to presser 0.4 here and deprecate the current slice apis, recommending using the presser ones thru as_mapped_slab instead
  5. see if it feels too awkward to go thru an extra type for all read and writes or not, if so we can add back a couple helper functions like the ones discussed above, if not we can leave as-is and fully remove the old ones

@MarijnS95
Copy link
Member Author

Sounds good!

CC @manon-traverse @Jasper-Bekkers :)

@Jasper-Bekkers
Copy link
Member

We could keep the (default enabled) presser feature in #138, and just not provide any mapping API at all if it is disabled

I think we should always provide a mapping api in order to not break this crate; however, I think we should support presser as a first class citizen and potentially even not provide any other paths. However, if we go that route, we should make sure that it's minimal impact on exisiting users, and has great upgrade documentation.

@MarijnS95
Copy link
Member Author

I think we should always provide a mapping api in order to not break this crate

Yeah, it would always go through presser in that case, and we should omit the feature flag altogether (if users really are against using presser in the public API, we could provide minimal mapping functions that are implemented on top of presser internally 😬).


For this to work out I suggest we follow @fu5ha's points above - drop this PR - and use at least the examples in this repo and our codebase to test-drive a full public-API migration to presser when point 4 is finished. We can always make beta releases or provide a non-breaking release with #[deprecated] statements, just to see how it all pans out without affecting anyone.

@fu5ha
Copy link
Contributor

fu5ha commented Oct 19, 2022

Sounds good to me. We can/will also use our internal codebase to test things out!

@djkoloski
Copy link

Would it be worth releasing a version that deprecates mapped_(mut_)?slice()? That would ensure that anyone using the current API should be using mapped_ptr() and size() instead.

@fu5ha
Copy link
Contributor

fu5ha commented Oct 31, 2022

@djkoloski yeah.. this PR is likely to be closed rather than merge now, see the current plan described in first comment of #138

@MarijnS95
Copy link
Member Author

@djkoloski That is effectively - exactly - what I wrote two posts above yours 😉. This is planned, I just have had too much on my plate to look into EmbarkStudios/presser#5 followed by #138, apologies @fu5ha I'll queue it up asap!

As discussed long ago, and recently in #138, it is undefined behaviour
to create or transmute to `&[u8]` when the underlying data is possibly
uninit.  This also holds true for transmuting arbitrary `T: Copy`
structures to `&[u8]` where eventual padding bytes are considered
uninitialized, hence invalid for `u8`.

Instead of coming up with a massive safety API that distinguishes
between uninitialized and initialized buffers - which turn out to be
really easy to invalidate by copying structures with padding bytes -
place the onus on the user to keep track of initialization status by
only ever providing mapped slices in an `unsafe` context.  Users are
expected to initialize the buffer using `ptr::copy(_nonoverlapping)()`
when used from a CPU context instead of calling `.mapped_mut_slice()`,
or switch to the new [presser] API from #138.

[presser]: https://crates.io/crates/presser
@MarijnS95 MarijnS95 closed this Nov 30, 2022
@MarijnS95 MarijnS95 deleted the mapped-slice-unsafe branch November 30, 2022 10:21
fu5ha added a commit to EmbarkStudios/presser that referenced this pull request Nov 6, 2023
Motivated by
Traverse-Research/gpu-allocator#139, scaffolded
some read-helper functions.

These helpers are mostly all unsafe and have both semi-checked and
completely unchecked variants. They are relevant in either case because
they lay out in documentation exactly what the needed safety
requirements are to read the given data, given that we have a properly
implemented `Slab`, and try to remove some common footguns (alignment,
size within allocation) where possible in the checked variants.

A note for reviewers is that you can skip the `copy.rs` file as that is
just code movement from the old `lib.rs`.

@eddyb, requested your review since you helped validate `presser`
originally and you might have some valuable input on the safety
comments/requirements here.
@MarijnS95 MarijnS95 restored the mapped-slice-unsafe branch January 8, 2025 10:11
@MarijnS95
Copy link
Member Author

I am reopening this PR, if not in the least for tracking purposes. While the presser changes were merged as-is (adding lots of documentation and a crate dependency which I have yet to digest, for what is effectively a raw pointer API), while leaving the original UB fn mapped_(mut_)slice() in place, and leaving it safe to call.

We'd have to remove this API or mark it unsafe, as well as provide the proposed MaybeUninit convenience accessors.

@MarijnS95 MarijnS95 reopened this Jan 8, 2025
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

Successfully merging this pull request may close these issues.

5 participants