Skip to content

mem::swap behaves incorrectly in CTFE (and Miri) #94371

Closed
@RalfJung

Description

@RalfJung

Since #94212, mem::swap behaves incorrectly during CTFE (where it is unstably accessible) and in Miri. This is demonstrated by the following code failing to build:

#![feature(const_swap)]
#![feature(const_mut_refs)]

use std::borrow::Cow;

pub enum NamePadding {
    PadNone,
    PadOnRight,
}

pub enum TestName {
    StaticTestName(&'static str),
    DynTestName(String),
    AlignedTestName(Cow<'static, str>, NamePadding),
}

pub enum ShouldPanic {
    No,
    Yes,
    YesWithMessage(&'static str),
}

pub enum TestType {
    UnitTest,
    IntegrationTest,
    DocTest,
    Unknown,
}

pub struct TestDesc {
    pub name: TestName,
    pub ignore: bool,
    pub should_panic: ShouldPanic,
    pub compile_fail: bool,
    pub no_run: bool,
    pub test_type: TestType,
}

pub struct Bencher;

pub enum TestFn {
    StaticTestFn(fn()),
    StaticBenchFn(fn(&mut Bencher)),
    DynTestFn(Box<dyn FnOnce() + Send>),
    DynBenchFn(Box<dyn Fn(&mut Bencher) + Send>),
}

pub struct TestDescAndFn {
    pub desc: TestDesc,
    pub testfn: TestFn,
}

pub struct TestId(pub usize);

type T = (TestId, TestDescAndFn);

const C: (T, T) = {
    let mut x = (
        TestId(0),
        TestDescAndFn {
            desc: TestDesc {
                name: TestName::StaticTestName("name"),
                ignore: true,
                should_panic: ShouldPanic::Yes,
                compile_fail: false,
                no_run: false,
                test_type: TestType::UnitTest,
            },
            testfn: TestFn::StaticTestFn(|| {}),
        },
    );
    let mut y = (
        TestId(0),
        TestDescAndFn {
            desc: TestDesc {
                name: TestName::StaticTestName("name"),
                ignore: true,
                should_panic: ShouldPanic::Yes,
                compile_fail: false,
                no_run: false,
                test_type: TestType::UnitTest,
            },
            testfn: TestFn::StaticTestFn(|| {}),
        },
    );
    std::mem::swap(&mut x, &mut y);
    (x, y)
};

fn main() {}

The error is:

error[E0080]: it is undefined behavior to use this value
  --> swap2.rs:57:1
   |
57 | / const C: (T, T) = {
58 | |   let mut x = (
59 | |             TestId(0),
60 | |             TestDescAndFn {
...  |
87 | |   (x, y)
88 | | };
   | |__^ type validation failed at .1.1.desc.name.<enum-tag>: encountered uninitialized bytes, but expected initialized plain (non-pointer) bytes
   |
   = note: The rules on what exactly is undefined behavior aren't clear, so this check might be overzealous. Please open an issue on the rustc repository if you believe it should not be considered undefined behavior.
   = note: the raw bytes of the constant (size: 208, align: 8) {
               0x00 │ 00 00 00 00 00 00 00 00 00 __ __ __ __ __ __ __ │ .........░░░░░░░
               0x10 │ ╾───────alloc14───────╼ 04 00 00 00 00 00 00 00 │ ╾──────╼........
               0x20 │ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ │ ░░░░░░░░░░░░░░░░
               0x30 │ 01 00 00 00 00 00 00 00 __ __ __ __ __ __ __ __ │ ........░░░░░░░░
               0x40 │ __ __ __ __ __ __ __ __ 01 00 00 00 __ __ __ __ │ ░░░░░░░░....░░░░
               0x50 │ 00 00 00 00 00 00 00 00 ╾───────alloc18───────╼ │ ........╾──────╼
               0x60 │ __ __ __ __ __ __ __ __ 00 00 00 00 00 00 00 00 │ ░░░░░░░░........
               0x70 │ __ __ __ __ __ __ __ __ ╾───────alloc4────────╼ │ ░░░░░░░░╾──────╼
               0x80 │ 04 00 00 00 00 00 00 00 __ __ __ __ __ __ __ __ │ ........░░░░░░░░
               0x90 │ __ __ __ __ __ __ __ __ 01 00 00 00 00 00 00 00 │ ░░░░░░░░........
               0xa0 │ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ │ ░░░░░░░░░░░░░░░░
               0xb0 │ __ __ __ __ __ __ __ __ 00 00 00 00 00 00 00 00 │ ░░░░░░░░........
               0xc0 │ ╾───────alloc8────────╼ __ __ __ __ __ __ __ __ │ ╾──────╼░░░░░░░░
           }

The underlying reason for this is #69488: the new swap copies the data in chunks of MaybeUninit<usize>, but this is implemented incorrectly in CTFE and Miri in the sense that if any of the bytes in the source of such a value is uninit, then the entire value becomes uninit after the copy.

This reproduces only with very particular types that hit certain code paths in swap; my attempts at writing a smaller example from scratch failed so instead I extracted this from the test harness where the problem originally occurred in Miri.

Also, even a fix #69488 would not fully resolve the problem: if data contains pointers at unaligned positions, then the usize chunking might attempt to copy a part of a pointer, which CTFE and Miri do not support (that's #87184, and it is a lot harder to fix than #69488). This was in theory already a problem with the previous implementation of swap, but there the chunks were bigger so pointers were less likely to cross the chunk boundary. I don't know if this is ever a problem in practice though. Miri/CTFE will halt when such a partial pointer copy is attempted, so we should know if/when that happens.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions