Skip to content

Commit c560774

Browse files
committed
c-variadic: more precise compatibility check in const-eval
1 parent 37d85e5 commit c560774

4 files changed

Lines changed: 486 additions & 87 deletions

File tree

compiler/rustc_const_eval/src/interpret/intrinsics.rs

Lines changed: 150 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ use std::assert_matches;
88

99
use rustc_abi::{FieldIdx, HasDataLayout, Size, VariantIdx};
1010
use rustc_apfloat::ieee::{Double, Half, Quad, Single};
11+
use rustc_ast::{IntTy, UintTy};
1112
use rustc_middle::mir::interpret::{CTFE_ALLOC_SALT, read_target_uint, write_target_uint};
1213
use rustc_middle::mir::{self, BinOp, ConstValue, NonDivergingIntrinsic};
1314
use rustc_middle::ty::layout::TyAndLayout;
@@ -21,9 +22,9 @@ use super::util::ensure_monomorphic_enough;
2122
use super::{
2223
AllocId, CheckInAllocMsg, ImmTy, InterpCx, InterpResult, Machine, OpTy, PlaceTy, Pointer,
2324
PointerArithmetic, Projectable, Provenance, Scalar, err_ub_format, err_unsup_format, interp_ok,
24-
throw_inval, throw_ub, throw_ub_format, throw_unsup_format,
25+
throw_inval, throw_ub, throw_ub_format,
2526
};
26-
use crate::interpret::Writeable;
27+
use crate::interpret::{MPlaceTy, Writeable};
2728

2829
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
2930
enum MulAddType {
@@ -56,6 +57,22 @@ pub(crate) enum MinMax {
5657
MaximumNumberNsz,
5758
}
5859

60+
/// Whether two types `T` and `U` are compatible when a value of type `T` is passed as a c-variadic
61+
/// argument and read as a value of type `U`.
62+
enum VarArgCompatible {
63+
/// `T` and `U` are compatible, e.g.
64+
///
65+
/// - They're the same type.
66+
/// - One is `usize`/`isize`, the other an integer type of the same width
67+
/// and sign on the current target.
68+
/// - They are compatible pointer types (see the exact rules below).
69+
Compatible,
70+
/// `T` and `U` are definitely not compatible.
71+
Incompatible,
72+
/// `T` and `U` are corresponding signed and unsigned integer types.
73+
CastIntTo { source_is_signed: bool },
74+
}
75+
5976
/// Directly returns an `Allocation` containing an absolute path representation of the given type.
6077
pub(crate) fn alloc_type_name<'tcx>(tcx: TyCtxt<'tcx>, ty: Ty<'tcx>) -> (AllocId, u64) {
6178
let path = crate::util::type_name(tcx, ty);
@@ -739,18 +756,13 @@ impl<'tcx, M: Machine<'tcx>> InterpCx<'tcx, M> {
739756
throw_ub!(VaArgOutOfBounds);
740757
};
741758

742-
// NOTE: In C some type conversions are allowed (e.g. casting between signed and
743-
// unsigned integers). For now we require c-variadic arguments to be read with the
744-
// exact type they were passed as.
745-
if arg_mplace.layout.ty != dest.layout.ty {
746-
throw_unsup_format!(
747-
"va_arg type mismatch: requested `{}`, but next argument is `{}`",
748-
dest.layout.ty,
749-
arg_mplace.layout.ty
750-
);
751-
}
752-
// Copy the argument.
753-
self.copy_op(&arg_mplace, dest)?;
759+
// Error when the caller's argument is not c-variadic compatible with the type
760+
// requested by the callee.
761+
self.validate_c_variadic_argument(&arg_mplace, dest.layout)?;
762+
763+
// Copy the argument, allowing a transmute and relying on the compatibility check
764+
// rejecting conversions between types of different size.
765+
self.copy_op_allow_transmute(&arg_mplace, dest)?;
754766

755767
// Update the VaList pointer.
756768
let new_key = self.va_list_ptr(varargs);
@@ -766,6 +778,130 @@ impl<'tcx, M: Machine<'tcx>> InterpCx<'tcx, M> {
766778
interp_ok(true)
767779
}
768780

781+
/// Validate whether the value and type passed by the caller are compatible with the type
782+
/// requested by the callee. Based on section 7.16.1.1 of the C23 specification.
783+
///
784+
/// The callee requesting a value of a type is valid when that type is compatible with the type
785+
/// provided by the caller (see `validate_c_variadic_compatible_ty`) and, if both types are
786+
/// integers of the same size but different signedness, the passed value must be representable
787+
/// in both types.
788+
fn validate_c_variadic_argument(
789+
&mut self,
790+
arg_mplace: &MPlaceTy<'tcx, M::Provenance>,
791+
callee_type: TyAndLayout<'tcx>,
792+
) -> InterpResult<'tcx> {
793+
let callee_ty = callee_type.ty;
794+
let caller_ty = arg_mplace.layout.ty;
795+
796+
// Identical types are clearly compatible.
797+
if caller_ty == callee_ty {
798+
return interp_ok(());
799+
}
800+
801+
// Types of different sizes can never be compatible.
802+
if arg_mplace.layout.size != callee_type.size {
803+
throw_ub_format!(
804+
"va_arg type mismatch: requested `{}` is incompatible with next argument of type `{}`",
805+
callee_ty,
806+
caller_ty,
807+
)
808+
}
809+
810+
match self.validate_c_variadic_compatible_ty(arg_mplace.layout.ty, callee_type.ty)? {
811+
VarArgCompatible::Compatible => interp_ok(()),
812+
VarArgCompatible::Incompatible => throw_ub_format!(
813+
"va_arg type mismatch: requested `{}` is incompatible with next argument of type `{}`",
814+
callee_ty,
815+
caller_ty,
816+
),
817+
VarArgCompatible::CastIntTo { source_is_signed } => {
818+
// Check that the value can be represented in the target type.
819+
let size = arg_mplace.layout.size;
820+
let scalar = self.read_scalar(arg_mplace)?;
821+
if scalar.to_int(size)? < 0 {
822+
throw_ub_format!(
823+
"va_arg value mismatch: value `{value}_{caller_ty}` cannot be represented by type `{callee_ty}`",
824+
value = if source_is_signed {
825+
scalar.to_int(size)?.to_string()
826+
} else {
827+
scalar.to_uint(size)?.to_string()
828+
}
829+
)
830+
}
831+
832+
interp_ok(())
833+
}
834+
}
835+
}
836+
837+
/// Check whether the caller and callee type are compatible for c-variadic calls. Further
838+
/// validation of the argument value may be needed to detect all UB.
839+
///
840+
/// Types `T` and `U` are compatible when:
841+
///
842+
/// - `T` and `U` are the same type.
843+
/// - `T` and `U` are integer types of the same size.
844+
/// - `T` and `U` are both pointers, and their target types are compatible.
845+
/// - `T` is a pointer to [`std::ffi::c_void`] and `U` is a pointer to [`i8`] or [`u8`],
846+
/// or vice versa.
847+
fn validate_c_variadic_compatible_ty(
848+
&mut self,
849+
caller_type: Ty<'tcx>,
850+
callee_type: Ty<'tcx>,
851+
) -> InterpResult<'tcx, VarArgCompatible> {
852+
if caller_type == callee_type {
853+
return interp_ok(VarArgCompatible::Compatible);
854+
}
855+
856+
if self.layout_of(caller_type)?.size != self.layout_of(callee_type)?.size {
857+
return interp_ok(VarArgCompatible::Incompatible);
858+
}
859+
860+
// Any character type (`char`, `unsigned char` and `signed char`) is compatible with
861+
// `void*`, so the signedness of `c_char` is irrelevant here.
862+
let is_c_char = |ty: Ty<'_>| matches!(ty.kind(), ty::Uint(UintTy::U8) | ty::Int(IntTy::I8));
863+
864+
match (caller_type.kind(), callee_type.kind()) {
865+
(ty::RawPtr(caller_target_ty, _), ty::RawPtr(callee_target_ty, _)) => {
866+
// In C, types can be qualified by a combination of `const`, `volatile` and
867+
// `restrict`. These properties are irrelevant for the ABI, and don't have an
868+
// equivalent in rust.
869+
870+
// Accept the cast if one type is pointer to void, and the other is a pointer to
871+
// a character type (`char`, `unsigned char` and `signed char`).
872+
if caller_target_ty.is_c_void(self.tcx.tcx) && is_c_char(*callee_target_ty) {
873+
return interp_ok(VarArgCompatible::Compatible);
874+
}
875+
if callee_target_ty.is_c_void(self.tcx.tcx) && is_c_char(*caller_target_ty) {
876+
return interp_ok(VarArgCompatible::Compatible);
877+
}
878+
879+
// Accept the cast if both types are pointers to compatible types.
880+
match self
881+
.validate_c_variadic_compatible_ty(*caller_target_ty, *callee_target_ty)?
882+
{
883+
VarArgCompatible::Incompatible => interp_ok(VarArgCompatible::Incompatible),
884+
VarArgCompatible::Compatible => interp_ok(VarArgCompatible::Compatible),
885+
VarArgCompatible::CastIntTo { source_is_signed: _ } => {
886+
// The integer cast check is not needed when the value is behind a pointer.
887+
interp_ok(VarArgCompatible::Compatible)
888+
}
889+
}
890+
}
891+
(ty::Int(_), ty::Uint(_)) => {
892+
interp_ok(VarArgCompatible::CastIntTo { source_is_signed: true })
893+
}
894+
(ty::Uint(_), ty::Int(_)) => {
895+
interp_ok(VarArgCompatible::CastIntTo { source_is_signed: false })
896+
}
897+
(ty::Int(_), ty::Int(_)) | (ty::Uint(_), ty::Uint(_)) => {
898+
// E.g. cast between `usize` and `u64` on a 64-bit platform.
899+
interp_ok(VarArgCompatible::Compatible)
900+
}
901+
_ => interp_ok(VarArgCompatible::Incompatible),
902+
}
903+
}
904+
769905
pub(super) fn eval_nondiverging_intrinsic(
770906
&mut self,
771907
intrinsic: &NonDivergingIntrinsic<'tcx>,

library/core/src/ffi/va_list.rs

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -390,11 +390,21 @@ impl<'f> VaList<'f> {
390390
///
391391
/// # Safety
392392
///
393-
/// This function is only sound to call when there is another argument to read, and that
394-
/// argument is a properly initialized value of the type `T`.
393+
/// This function is safe to call only if all of the following conditions are satisfied:
395394
///
396-
/// Calling this function with an incompatible type, an invalid value, or when there
397-
/// are no more variable arguments, is unsound.
395+
/// - There is another c-variadic argument to read.
396+
/// - The actual type of the argument `U` is compatible with `T` (as defined below).
397+
/// - If `U` and `T` are both integer types, then the value passed by the caller must be
398+
/// representable in both types.
399+
///
400+
/// Types `T` and `U` are compatible when:
401+
///
402+
/// - `T` and `U` are the same type.
403+
/// - `T` and `U` are integer types of the same size.
404+
/// - `T` and `U` are both pointers, and their target types are compatible.
405+
/// - `T` is a pointer to [`c_void`] and `U` is a pointer to [`i8`] or [`u8`], or vice versa.
406+
///
407+
/// [`c_void`]: core::ffi::c_void
398408
#[inline] // Avoid codegen when not used to help backends that don't support VaList.
399409
#[rustc_const_unstable(feature = "const_c_variadic", issue = "151787")]
400410
pub const unsafe fn next_arg<T: VaArgSafe>(&mut self) -> T {

tests/ui/consts/const-eval/c-variadic-fail.rs

Lines changed: 67 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
#![feature(const_destruct)]
77
#![feature(const_clone)]
88

9-
use std::ffi::VaList;
9+
use std::ffi::{VaList, c_char, c_void};
1010
use std::mem::MaybeUninit;
1111

1212
const unsafe extern "C" fn read_n<const N: usize>(mut ap: ...) {
@@ -37,7 +37,7 @@ const unsafe extern "C" fn read_as<T: core::ffi::VaArgSafe>(mut ap: ...) -> T {
3737
ap.next_arg::<T>()
3838
}
3939

40-
unsafe fn read_cast() {
40+
unsafe fn read_cast_numeric() {
4141
const { read_as::<i32>(1i32) };
4242
const { read_as::<u32>(1u32) };
4343

@@ -47,20 +47,76 @@ unsafe fn read_cast() {
4747
const { read_as::<i64>(1i64) };
4848
const { read_as::<u64>(1u64) };
4949

50+
// A cast between signed and unsigned is OK so long as both types can represent the value.
5051
const { read_as::<u32>(1i32) };
51-
//~^ ERROR va_arg type mismatch: requested `u32`, but next argument is `i32`
52-
5352
const { read_as::<i32>(1u32) };
54-
//~^ ERROR va_arg type mismatch: requested `i32`, but next argument is `u32`
53+
54+
type ConcreteUsize = cfg_select! {
55+
target_pointer_width = "16" => u16,
56+
target_pointer_width = "32" => u32,
57+
target_pointer_width = "64" => u64,
58+
};
59+
60+
type ConcreteIsize = cfg_select! {
61+
target_pointer_width = "16" => i16,
62+
target_pointer_width = "32" => i32,
63+
target_pointer_width = "64" => i64,
64+
};
65+
66+
const { read_as::<ConcreteUsize>(1usize) };
67+
const { read_as::<usize>(1 as ConcreteUsize) };
68+
69+
const { read_as::<ConcreteIsize>(-1isize) };
70+
const { read_as::<isize>(-1 as ConcreteIsize) };
71+
72+
const { read_as::<u32>(-1i32) };
73+
//~^ ERROR va_arg value mismatch: value `-1_i32` cannot be represented by type `u32`
74+
const { read_as::<u32>(i32::MIN) };
75+
//~^ ERROR va_arg value mismatch: value `-2147483648_i32` cannot be represented by type `u32`
76+
const { read_as::<i32>(u32::MAX) };
77+
//~^ ERROR va_arg value mismatch: value `4294967295_u32` cannot be represented by type `i32`
78+
const { read_as::<i32>(i32::MAX as u32 + 1) };
79+
//~^ ERROR va_arg value mismatch: value `2147483648_u32` cannot be represented by type `i32`
80+
const { read_as::<i64>(u64::MAX) };
81+
//~^ ERROR va_arg value mismatch: value `18446744073709551615_u64` cannot be represented by type `i64`
82+
const { read_as::<i64>(i64::MAX as u64 + 1) };
83+
//~^ ERROR va_arg value mismatch: value `9223372036854775808_u64` cannot be represented by type `i64`
5584

5685
const { read_as::<i32>(1u64) };
57-
//~^ ERROR va_arg type mismatch: requested `i32`, but next argument is `u64`
86+
//~^ ERROR va_arg type mismatch: requested `i32` is incompatible with next argument of type `u64`
5887

5988
const { read_as::<f64>(1i32) };
60-
//~^ ERROR va_arg type mismatch: requested `f64`, but next argument is `i32`
89+
//~^ ERROR va_arg type mismatch: requested `f64` is incompatible with next argument of type `i32`
90+
}
6191

62-
const { read_as::<*const u8>(1i32) };
63-
//~^ ERROR va_arg type mismatch: requested `*const u8`, but next argument is `i32`
92+
unsafe fn read_cast_pointer() {
93+
// A pointer mutability cast is OK.
94+
const { read_as::<*const i32>(std::ptr::dangling_mut::<i32>()) };
95+
const { read_as::<*mut i32>(std::ptr::dangling::<i32>()) };
96+
97+
// A pointer cast is OK between compatible types.
98+
const { read_as::<*const i32>(std::ptr::dangling::<u32>()) };
99+
const { read_as::<*const i32>(std::ptr::dangling_mut::<u32>()) };
100+
const { read_as::<*mut i32>(std::ptr::dangling::<u32>()) };
101+
const { read_as::<*mut i32>(std::ptr::dangling_mut::<u32>()) };
102+
103+
// Casting between pointers to i8/u8 and c_void is OK.
104+
const { read_as::<*const c_char>(std::ptr::dangling::<c_void>()) };
105+
const { read_as::<*const c_void>(std::ptr::dangling::<c_char>()) };
106+
const { read_as::<*const i8>(std::ptr::dangling::<c_void>()) };
107+
const { read_as::<*const c_void>(std::ptr::dangling::<i8>()) };
108+
const { read_as::<*const u8>(std::ptr::dangling::<c_void>()) };
109+
const { read_as::<*const c_void>(std::ptr::dangling::<u8>()) };
110+
111+
const { read_as::<*const u16>(std::ptr::dangling::<c_void>()) };
112+
//~^ ERROR va_arg type mismatch: requested `*const u16` is incompatible with next argument of type `*const c_void`
113+
const { read_as::<*const c_void>(std::ptr::dangling::<u16>()) };
114+
//~^ ERROR va_arg type mismatch: requested `*const c_void` is incompatible with next argument of type `*const u16`
115+
const { read_as::<*const u16>(std::ptr::dangling::<i32>()) };
116+
//~^ ERROR va_arg type mismatch: requested `*const u16` is incompatible with next argument of type `*const i32`
117+
118+
const { read_as::<*const u8>(1usize) };
119+
//~^ ERROR requested `*const u8` is incompatible with next argument of type `usize`
64120
}
65121

66122
fn use_after_free() {
@@ -138,7 +194,8 @@ fn drop_of_invalid() {
138194
fn main() {
139195
unsafe {
140196
read_too_many();
141-
read_cast();
197+
read_cast_numeric();
198+
read_cast_pointer();
142199
manual_copy_read();
143200
manual_copy_drop();
144201
manual_copy_forget();

0 commit comments

Comments
 (0)