Skip to content

Commit c689d32

Browse files
committed
Auto merge of #11981 - y21:eager_int_transmute, r=llogiq
new lint: `eager_transmute` A small but still hopefully useful lint that looks for patterns such as `(x < 5).then_some(transmute(x))`. This is almost certainly wrong because it evaluates the transmute eagerly and can lead to surprises such as the check being completely removed and always evaluating to `Some` no matter what `x` is (it is UB after all when the integer is not a valid bitpattern for the transmuted-to type). [Example](https://godbolt.org/z/xoY34fPzh). The user most likely meant to use `then` instead. I can't remember where I saw this but this is inspired by a real bug that happened in practice. This could probably be a correctness lint? changelog: new lint: [`eager_int_transmute`]
2 parents 677f8d8 + 08d8ca9 commit c689d32

File tree

8 files changed

+373
-1
lines changed

8 files changed

+373
-1
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -5101,6 +5101,7 @@ Released 2018-09-13
51015101
[`duplicate_mod`]: https://rust-lang.github.io/rust-clippy/master/index.html#duplicate_mod
51025102
[`duplicate_underscore_argument`]: https://rust-lang.github.io/rust-clippy/master/index.html#duplicate_underscore_argument
51035103
[`duration_subsec`]: https://rust-lang.github.io/rust-clippy/master/index.html#duration_subsec
5104+
[`eager_transmute`]: https://rust-lang.github.io/rust-clippy/master/index.html#eager_transmute
51045105
[`else_if_without_else`]: https://rust-lang.github.io/rust-clippy/master/index.html#else_if_without_else
51055106
[`empty_drop`]: https://rust-lang.github.io/rust-clippy/master/index.html#empty_drop
51065107
[`empty_enum`]: https://rust-lang.github.io/rust-clippy/master/index.html#empty_enum

clippy_lints/src/declared_lints.rs

+1
Original file line numberDiff line numberDiff line change
@@ -653,6 +653,7 @@ pub(crate) static LINTS: &[&crate::LintInfo] = &[
653653
crate::trait_bounds::TRAIT_DUPLICATION_IN_BOUNDS_INFO,
654654
crate::trait_bounds::TYPE_REPETITION_IN_BOUNDS_INFO,
655655
crate::transmute::CROSSPOINTER_TRANSMUTE_INFO,
656+
crate::transmute::EAGER_TRANSMUTE_INFO,
656657
crate::transmute::TRANSMUTES_EXPRESSIBLE_AS_PTR_CASTS_INFO,
657658
crate::transmute::TRANSMUTE_BYTES_TO_STR_INFO,
658659
crate::transmute::TRANSMUTE_FLOAT_TO_INT_INFO,

clippy_lints/src/lib.rs

+1
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
// FIXME: switch to something more ergonomic here, once available.
2323
// (Currently there is no way to opt into sysroot crates without `extern crate`.)
2424
extern crate pulldown_cmark;
25+
extern crate rustc_abi;
2526
extern crate rustc_arena;
2627
extern crate rustc_ast;
2728
extern crate rustc_ast_pretty;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
use clippy_utils::diagnostics::span_lint_and_then;
2+
use clippy_utils::ty::is_normalizable;
3+
use clippy_utils::{path_to_local, path_to_local_id};
4+
use rustc_abi::WrappingRange;
5+
use rustc_errors::Applicability;
6+
use rustc_hir::{Expr, ExprKind, Node};
7+
use rustc_lint::LateContext;
8+
use rustc_middle::ty::Ty;
9+
10+
use super::EAGER_TRANSMUTE;
11+
12+
fn peel_parent_unsafe_blocks<'tcx>(cx: &LateContext<'tcx>, expr: &'tcx Expr<'tcx>) -> Option<&'tcx Expr<'tcx>> {
13+
for (_, parent) in cx.tcx.hir().parent_iter(expr.hir_id) {
14+
match parent {
15+
Node::Block(_) => {},
16+
Node::Expr(e) if let ExprKind::Block(..) = e.kind => {},
17+
Node::Expr(e) => return Some(e),
18+
_ => break,
19+
}
20+
}
21+
None
22+
}
23+
24+
fn range_fully_contained(from: WrappingRange, to: WrappingRange) -> bool {
25+
to.contains(from.start) && to.contains(from.end)
26+
}
27+
28+
pub(super) fn check<'tcx>(
29+
cx: &LateContext<'tcx>,
30+
expr: &'tcx Expr<'tcx>,
31+
transmutable: &'tcx Expr<'tcx>,
32+
from_ty: Ty<'tcx>,
33+
to_ty: Ty<'tcx>,
34+
) -> bool {
35+
if let Some(then_some_call) = peel_parent_unsafe_blocks(cx, expr)
36+
&& let ExprKind::MethodCall(path, receiver, [arg], _) = then_some_call.kind
37+
&& cx.typeck_results().expr_ty(receiver).is_bool()
38+
&& path.ident.name == sym!(then_some)
39+
&& let ExprKind::Binary(_, lhs, rhs) = receiver.kind
40+
&& let Some(local_id) = path_to_local(transmutable)
41+
&& (path_to_local_id(lhs, local_id) || path_to_local_id(rhs, local_id))
42+
&& is_normalizable(cx, cx.param_env, from_ty)
43+
&& is_normalizable(cx, cx.param_env, to_ty)
44+
// we only want to lint if the target type has a niche that is larger than the one of the source type
45+
// e.g. `u8` to `NonZeroU8` should lint, but `NonZeroU8` to `u8` should not
46+
&& let Ok(from_layout) = cx.tcx.layout_of(cx.param_env.and(from_ty))
47+
&& let Ok(to_layout) = cx.tcx.layout_of(cx.param_env.and(to_ty))
48+
&& match (from_layout.largest_niche, to_layout.largest_niche) {
49+
(Some(from_niche), Some(to_niche)) => !range_fully_contained(from_niche.valid_range, to_niche.valid_range),
50+
(None, Some(_)) => true,
51+
(_, None) => false,
52+
}
53+
{
54+
span_lint_and_then(
55+
cx,
56+
EAGER_TRANSMUTE,
57+
expr.span,
58+
"this transmute is always evaluated eagerly, even if the condition is false",
59+
|diag| {
60+
diag.multipart_suggestion(
61+
"consider using `bool::then` to only transmute if the condition holds",
62+
vec![
63+
(path.ident.span, "then".into()),
64+
(arg.span.shrink_to_lo(), "|| ".into()),
65+
],
66+
Applicability::MaybeIncorrect,
67+
);
68+
},
69+
);
70+
true
71+
} else {
72+
false
73+
}
74+
}

clippy_lints/src/transmute/mod.rs

+60-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
mod crosspointer_transmute;
2+
mod eager_transmute;
23
mod transmute_float_to_int;
34
mod transmute_int_to_bool;
45
mod transmute_int_to_char;
@@ -463,6 +464,62 @@ declare_clippy_lint! {
463464
"transmute results in a null function pointer, which is undefined behavior"
464465
}
465466

467+
declare_clippy_lint! {
468+
/// ### What it does
469+
/// Checks for integer validity checks, followed by a transmute that is (incorrectly) evaluated
470+
/// eagerly (e.g. using `bool::then_some`).
471+
///
472+
/// ### Why is this bad?
473+
/// Eager evaluation means that the `transmute` call is executed regardless of whether the condition is true or false.
474+
/// This can introduce unsoundness and other subtle bugs.
475+
///
476+
/// ### Example
477+
/// Consider the following function which is meant to convert an unsigned integer to its enum equivalent via transmute.
478+
///
479+
/// ```no_run
480+
/// #[repr(u8)]
481+
/// enum Opcode {
482+
/// Add = 0,
483+
/// Sub = 1,
484+
/// Mul = 2,
485+
/// Div = 3
486+
/// }
487+
///
488+
/// fn int_to_opcode(op: u8) -> Option<Opcode> {
489+
/// (op < 4).then_some(unsafe { std::mem::transmute(op) })
490+
/// }
491+
/// ```
492+
/// This may appear fine at first given that it checks that the `u8` is within the validity range of the enum,
493+
/// *however* the transmute is evaluated eagerly, meaning that it executes even if `op >= 4`!
494+
///
495+
/// This makes the function unsound, because it is possible for the caller to cause undefined behavior
496+
/// (creating an enum with an invalid bitpattern) entirely in safe code only by passing an incorrect value,
497+
/// which is normally only a bug that is possible in unsafe code.
498+
///
499+
/// One possible way in which this can go wrong practically is that the compiler sees it as:
500+
/// ```rust,ignore (illustrative)
501+
/// let temp: Foo = unsafe { std::mem::transmute(op) };
502+
/// (0 < 4).then_some(temp)
503+
/// ```
504+
/// and optimizes away the `(0 < 4)` check based on the assumption that since a `Foo` was created from `op` with the validity range `0..3`,
505+
/// it is **impossible** for this condition to be false.
506+
///
507+
/// In short, it is possible for this function to be optimized in a way that makes it [never return `None`](https://godbolt.org/z/ocrcenevq),
508+
/// even if passed the value `4`.
509+
///
510+
/// This can be avoided by instead using lazy evaluation. For the example above, this should be written:
511+
/// ```rust,ignore (illustrative)
512+
/// fn int_to_opcode(op: u8) -> Option<Opcode> {
513+
/// (op < 4).then(|| unsafe { std::mem::transmute(op) })
514+
/// ^^^^ ^^ `bool::then` only executes the closure if the condition is true!
515+
/// }
516+
/// ```
517+
#[clippy::version = "1.76.0"]
518+
pub EAGER_TRANSMUTE,
519+
correctness,
520+
"eager evaluation of `transmute`"
521+
}
522+
466523
pub struct Transmute {
467524
msrv: Msrv,
468525
}
@@ -484,6 +541,7 @@ impl_lint_pass!(Transmute => [
484541
TRANSMUTE_UNDEFINED_REPR,
485542
TRANSMUTING_NULL,
486543
TRANSMUTE_NULL_TO_FN,
544+
EAGER_TRANSMUTE,
487545
]);
488546
impl Transmute {
489547
#[must_use]
@@ -530,7 +588,8 @@ impl<'tcx> LateLintPass<'tcx> for Transmute {
530588
| transmute_float_to_int::check(cx, e, from_ty, to_ty, arg, const_context)
531589
| transmute_num_to_bytes::check(cx, e, from_ty, to_ty, arg, const_context)
532590
| (unsound_collection_transmute::check(cx, e, from_ty, to_ty)
533-
|| transmute_undefined_repr::check(cx, e, from_ty, to_ty));
591+
|| transmute_undefined_repr::check(cx, e, from_ty, to_ty))
592+
| (eager_transmute::check(cx, e, arg, from_ty, to_ty));
534593

535594
if !linted {
536595
transmutes_expressible_as_ptr_casts::check(cx, e, from_ty, from_ty_adjusted, to_ty, arg);

tests/ui/eager_transmute.fixed

+72
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
#![feature(rustc_attrs)]
2+
#![warn(clippy::eager_transmute)]
3+
#![allow(clippy::transmute_int_to_non_zero)]
4+
5+
use std::num::NonZeroU8;
6+
7+
#[repr(u8)]
8+
enum Opcode {
9+
Add = 0,
10+
Sub = 1,
11+
Mul = 2,
12+
Div = 3,
13+
}
14+
15+
fn int_to_opcode(op: u8) -> Option<Opcode> {
16+
(op < 4).then(|| unsafe { std::mem::transmute(op) })
17+
}
18+
19+
fn f(op: u8, unrelated: u8) {
20+
true.then_some(unsafe { std::mem::transmute::<_, Opcode>(op) });
21+
(unrelated < 4).then_some(unsafe { std::mem::transmute::<_, Opcode>(op) });
22+
(op < 4).then(|| unsafe { std::mem::transmute::<_, Opcode>(op) });
23+
(op > 4).then(|| unsafe { std::mem::transmute::<_, Opcode>(op) });
24+
(op == 0).then(|| unsafe { std::mem::transmute::<_, Opcode>(op) });
25+
}
26+
27+
unsafe fn f2(op: u8) {
28+
(op < 4).then(|| std::mem::transmute::<_, Opcode>(op));
29+
}
30+
31+
#[rustc_layout_scalar_valid_range_end(254)]
32+
struct NonMaxU8(u8);
33+
#[rustc_layout_scalar_valid_range_end(254)]
34+
#[rustc_layout_scalar_valid_range_start(1)]
35+
struct NonZeroNonMaxU8(u8);
36+
37+
macro_rules! impls {
38+
($($t:ty),*) => {
39+
$(
40+
impl PartialEq<u8> for $t {
41+
fn eq(&self, other: &u8) -> bool {
42+
self.0 == *other
43+
}
44+
}
45+
impl PartialOrd<u8> for $t {
46+
fn partial_cmp(&self, other: &u8) -> Option<std::cmp::Ordering> {
47+
self.0.partial_cmp(other)
48+
}
49+
}
50+
)*
51+
};
52+
}
53+
impls!(NonMaxU8, NonZeroNonMaxU8);
54+
55+
fn niche_tests(v1: u8, v2: NonZeroU8, v3: NonZeroNonMaxU8) {
56+
// u8 -> NonZeroU8, do lint
57+
let _: Option<NonZeroU8> = (v1 > 0).then(|| unsafe { std::mem::transmute(v1) });
58+
59+
// NonZeroU8 -> u8, don't lint, target type has no niche and therefore a higher validity range
60+
let _: Option<u8> = (v2 > NonZeroU8::new(1).unwrap()).then_some(unsafe { std::mem::transmute(v2) });
61+
62+
// NonZeroU8 -> NonMaxU8, do lint, different niche
63+
let _: Option<NonMaxU8> = (v2 < NonZeroU8::new(255).unwrap()).then(|| unsafe { std::mem::transmute(v2) });
64+
65+
// NonZeroNonMaxU8 -> NonMaxU8, don't lint, target type has more validity
66+
let _: Option<NonMaxU8> = (v3 < 255).then_some(unsafe { std::mem::transmute(v2) });
67+
68+
// NonZeroU8 -> NonZeroNonMaxU8, do lint, target type has less validity
69+
let _: Option<NonZeroNonMaxU8> = (v2 < NonZeroU8::new(255).unwrap()).then(|| unsafe { std::mem::transmute(v2) });
70+
}
71+
72+
fn main() {}

tests/ui/eager_transmute.rs

+72
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
#![feature(rustc_attrs)]
2+
#![warn(clippy::eager_transmute)]
3+
#![allow(clippy::transmute_int_to_non_zero)]
4+
5+
use std::num::NonZeroU8;
6+
7+
#[repr(u8)]
8+
enum Opcode {
9+
Add = 0,
10+
Sub = 1,
11+
Mul = 2,
12+
Div = 3,
13+
}
14+
15+
fn int_to_opcode(op: u8) -> Option<Opcode> {
16+
(op < 4).then_some(unsafe { std::mem::transmute(op) })
17+
}
18+
19+
fn f(op: u8, unrelated: u8) {
20+
true.then_some(unsafe { std::mem::transmute::<_, Opcode>(op) });
21+
(unrelated < 4).then_some(unsafe { std::mem::transmute::<_, Opcode>(op) });
22+
(op < 4).then_some(unsafe { std::mem::transmute::<_, Opcode>(op) });
23+
(op > 4).then_some(unsafe { std::mem::transmute::<_, Opcode>(op) });
24+
(op == 0).then_some(unsafe { std::mem::transmute::<_, Opcode>(op) });
25+
}
26+
27+
unsafe fn f2(op: u8) {
28+
(op < 4).then_some(std::mem::transmute::<_, Opcode>(op));
29+
}
30+
31+
#[rustc_layout_scalar_valid_range_end(254)]
32+
struct NonMaxU8(u8);
33+
#[rustc_layout_scalar_valid_range_end(254)]
34+
#[rustc_layout_scalar_valid_range_start(1)]
35+
struct NonZeroNonMaxU8(u8);
36+
37+
macro_rules! impls {
38+
($($t:ty),*) => {
39+
$(
40+
impl PartialEq<u8> for $t {
41+
fn eq(&self, other: &u8) -> bool {
42+
self.0 == *other
43+
}
44+
}
45+
impl PartialOrd<u8> for $t {
46+
fn partial_cmp(&self, other: &u8) -> Option<std::cmp::Ordering> {
47+
self.0.partial_cmp(other)
48+
}
49+
}
50+
)*
51+
};
52+
}
53+
impls!(NonMaxU8, NonZeroNonMaxU8);
54+
55+
fn niche_tests(v1: u8, v2: NonZeroU8, v3: NonZeroNonMaxU8) {
56+
// u8 -> NonZeroU8, do lint
57+
let _: Option<NonZeroU8> = (v1 > 0).then_some(unsafe { std::mem::transmute(v1) });
58+
59+
// NonZeroU8 -> u8, don't lint, target type has no niche and therefore a higher validity range
60+
let _: Option<u8> = (v2 > NonZeroU8::new(1).unwrap()).then_some(unsafe { std::mem::transmute(v2) });
61+
62+
// NonZeroU8 -> NonMaxU8, do lint, different niche
63+
let _: Option<NonMaxU8> = (v2 < NonZeroU8::new(255).unwrap()).then_some(unsafe { std::mem::transmute(v2) });
64+
65+
// NonZeroNonMaxU8 -> NonMaxU8, don't lint, target type has more validity
66+
let _: Option<NonMaxU8> = (v3 < 255).then_some(unsafe { std::mem::transmute(v2) });
67+
68+
// NonZeroU8 -> NonZeroNonMaxU8, do lint, target type has less validity
69+
let _: Option<NonZeroNonMaxU8> = (v2 < NonZeroU8::new(255).unwrap()).then_some(unsafe { std::mem::transmute(v2) });
70+
}
71+
72+
fn main() {}

0 commit comments

Comments
 (0)