Skip to content

Undefined behaviour in slice::fill specialization. #87891

New issue

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

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

Already on GitHub? # to your account

Closed
m-ou-se opened this issue Aug 9, 2021 · 9 comments · Fixed by #87892 or #88507
Closed

Undefined behaviour in slice::fill specialization. #87891

m-ou-se opened this issue Aug 9, 2021 · 9 comments · Fixed by #87892 or #88507
Labels
A-raw-pointers Area: raw pointers, MaybeUninit, NonNull A-slice Area: `[T]` E-easy Call for participation: Easy difficulty. Experience needed to fix: Not much. Good first issue. E-needs-test Call for participation: An issue has been fixed and does not reproduce, but no test has been added. T-libs Relevant to the library team, which will review and decide on the PR/issue.

Comments

@m-ou-se
Copy link
Member

m-ou-se commented Aug 9, 2021

This was reported by @the8472 on Zulip: https://rust-lang.zulipchat.com/#narrow/stream/219381-t-libs/topic/potential.20UB.20in.20slice.3A.3Afill/near/248871405

I'm wondering if the optimization here is correct:

if size_of::<T>() == 1 {
// SAFETY: The size_of check above ensures that values are 1 byte wide, as required
// for the transmute and write_bytes
unsafe {
let value: u8 = transmute_copy(&value);
write_bytes(self.as_mut_ptr(), value, self.len());
}

Specifically if T is MaybeUninit<u8>. It momentarily transmutes that to an u8 before passing it to write_bytes.

Demo:

use std::mem::MaybeUninit;

fn main() {
    let mut a = [MaybeUninit::<u8>::uninit(); 10];
    a.fill(MaybeUninit::uninit());
}
error: Undefined Behavior: using uninitialized data, but this operation requires initialized memory
    --> /home/mara/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/intrinsics.rs:2191:14
     |
2191 |     unsafe { write_bytes(dst, val, count) }
     |              ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ using uninitialized data, but this operation requires initialized memory
     |
     = help: this indicates a bug in the program: it performed an invalid operation, and caused Undefined Behavior
     = help: see https://doc.rust-lang.org/nightly/reference/behavior-considered-undefined.html for further information
@m-ou-se m-ou-se added I-unsound Issue: A soundness hole (worst kind of bug), see: https://en.wikipedia.org/wiki/Soundness I-prioritize Issue: Indicates that prioritization has been requested for this issue. T-libs Relevant to the library team, which will review and decide on the PR/issue. A-slice Area: `[T]` A-raw-pointers Area: raw pointers, MaybeUninit, NonNull labels Aug 9, 2021
@Aaron1011
Copy link
Member

Aaron1011 commented Aug 9, 2021

I think the fix should be as simple as using MaybeUninit<u8> instead of u8 as the type of value - that will work regardless of whether or not the memory is initialized.

@m-ou-se
Copy link
Member Author

m-ou-se commented Aug 9, 2021

The other option is just removing the if size_of::<T>() == 1 case entirely. Llvm is smart enough to turn that loop in the else branch into a memset.

@the8472
Copy link
Member

the8472 commented Aug 9, 2021

It does help the gcc codgen backend at least as it can't manage to produce a memset for u64 so I assume it would fail to do the same for u8 if it weren't for that specialization.

https://rust.godbolt.org/z/hrMYvWz9n

@m-ou-se
Copy link
Member Author

m-ou-se commented Aug 9, 2021

@the8472: It also fails for the u8 case: https://rust.godbolt.org/z/Wjx5Mnj5K

@the8472
Copy link
Member

the8472 commented Aug 9, 2021

It does call memset if you don't pass an array. https://rust.godbolt.org/z/x4KT99heT

@RalfJung
Copy link
Member

I'd think the write_bytes intrinsic could easily have its type changed to take a MaybeUninit<u8>, which would let one solve this problem. The stable function of course cannot be changed, though...

@bors bors closed this as completed in 362e0f5 Aug 11, 2021
@RalfJung RalfJung added the E-needs-test Call for participation: An issue has been fixed and does not reproduce, but no test has been added. label Aug 11, 2021
@RalfJung
Copy link
Member

Reopening to track adding a test. (That's the standard procedure for this, isn't it?)

@RalfJung RalfJung reopened this Aug 11, 2021
@RalfJung RalfJung added E-easy Call for participation: Easy difficulty. Experience needed to fix: Not much. Good first issue. and removed I-unsound Issue: A soundness hole (worst kind of bug), see: https://en.wikipedia.org/wiki/Soundness I-prioritize Issue: Indicates that prioritization has been requested for this issue. labels Aug 12, 2021
@atsuzaki
Copy link
Contributor

@RalfJung I would like to try working on adding the test, though this will be my first contribution so I'd need some guidance. I'm assuming this would be a miri test as it's related to correctness--may I DM you on Zulip? (or would someone else be more suitable?)

@RalfJung
Copy link
Member

@atsuzaki great. :)

Miri runs the libcore test suite via https://github.com/rust-lang/miri-test-libstd. So adding it as a test in libcore makes most sense, IMO.

# for free to join this conversation on GitHub. Already have an account? # to comment
Labels
A-raw-pointers Area: raw pointers, MaybeUninit, NonNull A-slice Area: `[T]` E-easy Call for participation: Easy difficulty. Experience needed to fix: Not much. Good first issue. E-needs-test Call for participation: An issue has been fixed and does not reproduce, but no test has been added. T-libs Relevant to the library team, which will review and decide on the PR/issue.
Projects
None yet
5 participants