Skip to content

Replace std::mem::uninitialized with MaybeUninit#1027

Merged
goddessfreya merged 5 commits intorust-windowing:masterfrom
murarth:maybe-uninit
Jul 11, 2019
Merged

Replace std::mem::uninitialized with MaybeUninit#1027
goddessfreya merged 5 commits intorust-windowing:masterfrom
murarth:maybe-uninit

Conversation

@murarth
Copy link
Copy Markdown
Contributor

@murarth murarth commented Jul 8, 2019

Closes #1025

  • Tested on all platforms changed
  • cargo fmt has been run on this branch
  • Added an entry to CHANGELOG.md if knowledge of this change could be valuable to users
  • Updated documentation to reflect any user-facing changes, including notes of platform-specific behavior
  • Created or updated an example program if it would help users understand this functionality
  • Updated feature matrix, if new features were added or implemented

@murarth
Copy link
Copy Markdown
Contributor Author

murarth commented Jul 8, 2019

All uses of mem::uninitialized were in Xlib FFI code, so Linux X11 is the only platform affected by this change.

@goddessfreya
Copy link
Copy Markdown
Contributor

goddessfreya commented Jul 8, 2019

I'm not sure who thought MaybeUninit is better than mem::uninitialized... it seams easy to mess up, unlike the prior mem::uninitialized api...

@murarth
Copy link
Copy Markdown
Contributor Author

murarth commented Jul 8, 2019

I was not aware that even an uninitialized i32 is undefined behavior. I'll address those issues accordingly.

@felixrabe
Copy link
Copy Markdown
Contributor

I'm not sure who thought MaybeUninit is better than mem::uninitialized... it seams easy to mess up, unlike the prior mem::uninitialized api...

You know you can "delete" a comment :)

@@ -25,16 +25,16 @@ impl From<ffi::XIModifierState> for ModifiersState {

pub struct PointerState<'a> {
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

It turns out that some of these private fields are unused. rustc wasn't detecting this before because the struct was being filled with &mut pointer_state.field references and this was seen as a use. I've commented out the fields as it should have no effect on the program. I could instead make them public or add #[allow(dead_code)] to the struct.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I kept them in both 1) to make allocation easier, and 2) so that contributors can more easily discover/utilize functionality they might need (the idea is to provide a robust internal API). I'd prefer they be kept in, though with comments explaining the rationale.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fair enough. I've restored the fields and marked them pub to silence the warnings.

@murarth
Copy link
Copy Markdown
Contributor Author

murarth commented Jul 8, 2019

@zegentzy I believe I have resolved all of the undefined behavior issues.

@aloucks
Copy link
Copy Markdown
Contributor

aloucks commented Jul 8, 2019

There should probably be a more thorough audit of mem::uninitialized.

I think that most of these cases should either be replaced with mem::zeroed() (e.g. for native struct initialization) or simply use 0 or ptr::null_mut() to initialize integers and pointers. Arrays should probably just use [0; the_size] as well. Uninitialized memory is playing with fire and a recipe for UB. We should try to avoid MaybeUninit unless it's absolutely necessary.

@august64
Copy link
Copy Markdown
Member

august64 commented Jul 8, 2019

@aloucks in what ways is zeroed safer than uninitialized?

Copy link
Copy Markdown
Contributor

@goddessfreya goddessfreya left a comment

Choose a reason for hiding this comment

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

This code looks about right. Can we try replacing some of them with MaybeUninit::zeroed()?

@goddessfreya
Copy link
Copy Markdown
Contributor

goddessfreya commented Jul 8, 2019

@francesca64 Reading from unitialized memory triggers UB. Reading from zero-initialized memory only triggers UB if that is not a valid state. Since most (all?) of these variables are integers, accidentally reading from them if they get zeroed-initialized, instead of uninitialized, will help us avoid accidental UB.

@aloucks
Copy link
Copy Markdown
Contributor

aloucks commented Jul 8, 2019

@aloucks in what ways is mem::zeroed safer than MaybeUninit?

In general, you should avoid mem::zeroed() too, but sometimes it's you're only option. (e.g. https://github.com/retep998/winapi-rs#how-do-i-create-an-instance-of-a-union). It's usually fine for initializing native structs (because they generally don't contain references).

Why would you prefer MaybeUninit over initializing pointers and integers to zero/null?

@aloucks
Copy link
Copy Markdown
Contributor

aloucks commented Jul 8, 2019

Reading from zero-initialized memory only triggers UB if that is not a valid state.

I don't think you even have to read from it for it to be considered UB. The addition of MaybeUninit doesn't make uninitialized memory safe. It was just impossible to use mem::uninitialized() in some cases before without triggering UB (e.g. with bools). MaybeUninit provides a for a way to to do it safely, but it's still extremely easy to get it wrong.

@goddessfreya
Copy link
Copy Markdown
Contributor

@aloucks According to the docs:

It depends on T whether that already makes for proper initialization. For example, MaybeUninit::zeroed() is initialized, but MaybeUninit<&'static i32>::zeroed() is not because references must not be null.

So, operations that would normally trigger UB on uninitialized memory will not trigger UB on zero-initialized types, if zero is a valid initialization of the type.

@goddessfreya
Copy link
Copy Markdown
Contributor

goddessfreya commented Jul 9, 2019

Why would you prefer MaybeUninit over initializing pointers and integers to zero/null?

I'd rather we zero initialize whole integer-only structs than manually set their contents to zero.

@aloucks
Copy link
Copy Markdown
Contributor

aloucks commented Jul 9, 2019

So, operations that would normally trigger UB on uninitialized memory will not trigger UB on zero-initialized types, if zero is a valid initialization of the type.

Yes, absolutely - that's why I was saying prefer it over uninitialized and prefer 0 or ptr::null_mut() for non-struct members.

@aloucks
Copy link
Copy Markdown
Contributor

aloucks commented Jul 9, 2019

MaybeUninit::zeroed().assume_init() is no different than mem::zeroed().

@murarth
Copy link
Copy Markdown
Contributor Author

murarth commented Jul 9, 2019

MaybeUninit::zeroed().assume_init() is no different than mem::zeroed().

Yes, but since mem::uninitialized has been deprecated, mem::zeroed may also be deprecated in the future and using MaybeUninit::zeroed now avoids making another change in the future.

That said, if brevity is preferred, I'm not opposed to changing it.

@aloucks
Copy link
Copy Markdown
Contributor

aloucks commented Jul 9, 2019

What I had originally meant by auditing the use of mem::uninitialized() is that many of these cases don't require it at all and can actually be expressed with safe code.

For example, XConnection::query_pointer can be implemented without it and the only unsafe part is where it calls the xinput2.XIQueryPointer ffi function.

pub fn query_pointer(
    &self,
    window: ffi::Window,
    device_id: c_int,
) -> Result<PointerState<'_>, XError> {
    let mut root = 0;
    let mut child = 0;
    let mut root_x = 0.0;
    let mut root_y = 0.0;
    let mut win_x = 0.0;
    let mut win_y = 0.0;
    let mut group = Default::default();
    let mut buttons = Default::default();
    let mut modifiers = Default::default();

    let relative_to_window = unsafe { (self.xinput2.XIQueryPointer)(
        self.display,
        device_id,
        window,
        &mut root,
        &mut child,
        &mut root_x,
        &mut root_y,
        &mut win_x,
        &mut win_y,
        &mut buttons,
        &mut modifiers,
        &mut group,
    ) == ffi::True };

    if let Err(err) = self.check_errors() {
        Err(err)
    } else {
        Ok(PointerState {
            xconn: self,
            root,
            child,
            root_x,
            root_y,
            win_x,
            relative_to_window,
            buttons,
            modifiers,
            win_y,
            group
        })
    }
}

Interestingly enough now that we're creating that struct with 100% safe code we find that most of those fields aren't even being used. The overzealous usage of unsafe hid them from the compiler. We shouldn't blindly swap mem::uninitialized with MaybeUninit, instead we should remove it's usage everywhere that isn't 100% necessary.

warning: field is never used: `root`
  --> src/platform_impl/linux/x11/util/input.rs:28:5
   |
28 |     root: ffi::Window,
   |     ^^^^^^^^^^^^^^^^^
   |
   = note: #[warn(dead_code)] on by default

warning: field is never used: `child`
  --> src/platform_impl/linux/x11/util/input.rs:29:5
   |
29 |     child: ffi::Window,
   |     ^^^^^^^^^^^^^^^^^^

warning: field is never used: `win_x`
  --> src/platform_impl/linux/x11/util/input.rs:32:5
   |
32 |     win_x: c_double,
   |     ^^^^^^^^^^^^^^^

warning: field is never used: `win_y`
  --> src/platform_impl/linux/x11/util/input.rs:33:5
   |
33 |     win_y: c_double,
   |     ^^^^^^^^^^^^^^^

warning: field is never used: `group`
  --> src/platform_impl/linux/x11/util/input.rs:36:5
   |
36 |     group: ffi::XIGroupState,
   |     ^^^^^^^^^^^^^^^^^^^^^^^^

warning: field is never used: `relative_to_window`
  --> src/platform_impl/linux/x11/util/input.rs:37:5
   |
37 |     relative_to_window: bool,
   |     ^^^^^^^^^^^^^^^^^^^^^^^^

* Prefer safe zero-initialization using `Default`, when possible
* Zero-initialize integers and floats using `0` or `0.0`
* Use `MaybeUninit::uninit` for large byte buffers and union types
* Use `MaybeUninit::uninit` when the resulting value is ignored
@murarth
Copy link
Copy Markdown
Contributor Author

murarth commented Jul 9, 2019

I've pushed a commit reforming usage of MaybeUninit, following these guidelines:

  • Prefer safe zero-initialization using Default, when possible
  • Zero-initialize integers and floats using 0 or 0.0
  • Use MaybeUninit::uninit for large byte buffers and union types
  • Use MaybeUninit::uninit when the resulting value is ignored

if let Ok(ref xconn) = *xconn_lock {
let mut buf: [c_char; 1024] = mem::uninitialized();
// `assume_init` is safe here because the array consists of `MaybeUninit` values,
// which do not require initialization.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This could be replaced with

let mut buf = [0 as c_char; 1024];

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Why zero-initialize a KB of memory? If we can't trust a C library to copy data into a buffer and accurately report the number of bytes written, why are we using C libraries at all?

pub fn get_normal_hints(&self, window: ffi::Window) -> Result<NormalHints<'_>, XError> {
let size_hints = self.alloc_size_hints();
let mut supplied_by_user: c_long = unsafe { mem::uninitialized() };
let mut supplied_by_user = MaybeUninit::uninit();
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This is just an integer. There is no need for uninit.

You can just set it normally (and change the param back to &mut supplied_by_user below:

let mut supplied_by_user = 0;

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

The value is ignored. There's no unsafe. There's no undefined behavior.

Why zero-initialize a variable that is never read?

let (_, _, new_count) = self.lookup_utf8_inner(ic, key_event, &mut buffer);
// `assume_init` is safe here because the array consists of `MaybeUninit` values,
// which do not require initialization.
let mut buffer: [MaybeUninit<u8>; TEXT_BUFFER_SIZE] =
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This could be replaced with

let mut buffer = [0_u8; TEXT_BUFFER_SIZE];

// 64 would suffice for Linux, but 256 will be enough everywhere (as per SUSv2). For instance, this is
// the limit defined by OpenBSD.
const MAXHOSTNAMELEN: usize = 256;
let mut hostname: [c_char; MAXHOSTNAMELEN] = mem::uninitialized();
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This could be replaced with

let mut hostname = [0 as c_char; MAXHOSTNAMELEN];

let cursor = unsafe {
// We don't care about this color, since it only fills bytes
// in the pixmap which are not 0 in the mask.
let dummy_color: ffi::XColor = mem::uninitialized();
Copy link
Copy Markdown
Contributor

@aloucks aloucks Jul 10, 2019

Choose a reason for hiding this comment

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

Why not mem::zeroed() and be done with it? (or Default::default(), which is probably the same)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Same as above, the value is not used.

Copy link
Copy Markdown
Contributor

@goddessfreya goddessfreya left a comment

Choose a reason for hiding this comment

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

@murarth
Copy link
Copy Markdown
Contributor Author

murarth commented Jul 11, 2019

Apologies if I've missed something. Are there any unresolved issues with this PR?

@goddessfreya goddessfreya merged commit 7daf146 into rust-windowing:master Jul 11, 2019
@murarth murarth deleted the maybe-uninit branch July 11, 2019 16:40
felixrabe pushed a commit to felixrabe/winit that referenced this pull request Jul 11, 2019
…1027)

* Replace `std::mem::uninitialized` with `MaybeUninit`

* Avoid undefined behavior when using `MaybeUninit`

* Restore unused `PointerState` fields as internally public

* Zero-initialize some struct values in Xlib FFI calls

* Reform usage of `MaybeUninit` in Xlib FFI

* Prefer safe zero-initialization using `Default`, when possible
* Zero-initialize integers and floats using `0` or `0.0`
* Use `MaybeUninit::uninit` for large byte buffers and union types
* Use `MaybeUninit::uninit` when the resulting value is ignored
chrisduerr added a commit to chrisduerr/winit that referenced this pull request Aug 31, 2019
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

Replace mem::uninitialized<T>() with mem::MaybeUninit<T>

5 participants