Skip to content

Commit 6aedccd

Browse files
committed
c-variadic: more precise compatibility check in const-eval
1 parent 68ffae4 commit 6aedccd

4 files changed

Lines changed: 330 additions & 84 deletions

File tree

compiler/rustc_const_eval/src/interpret/intrinsics.rs

Lines changed: 161 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,14 @@ pub(crate) enum MinMax {
5657
MaximumNumberNsz,
5758
}
5859

60+
enum VarArgCompatible {
61+
Identical,
62+
Incompatible,
63+
CastSignedToUnsigned(IntTy),
64+
CastUnsignedToSigned(UintTy),
65+
ValidPtrCast,
66+
}
67+
5968
/// Directly returns an `Allocation` containing an absolute path representation of the given type.
6069
pub(crate) fn alloc_type_name<'tcx>(tcx: TyCtxt<'tcx>, ty: Ty<'tcx>) -> (AllocId, u64) {
6170
let path = crate::util::type_name(tcx, ty);
@@ -739,18 +748,13 @@ impl<'tcx, M: Machine<'tcx>> InterpCx<'tcx, M> {
739748
throw_ub!(VaArgOutOfBounds);
740749
};
741750

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)?;
751+
// Error when the caller's argument is not c-variadic compatible with the type
752+
// requested by the callee.
753+
self.validate_c_variadic_compatible(&arg_mplace, dest.layout)?;
754+
755+
// Copy the argument, allowing a transmute and relying on the compatibility check
756+
// rejecting conversions between types of different size.
757+
self.copy_op_allow_transmute(&arg_mplace, dest)?;
754758

755759
// Update the VaList pointer.
756760
let new_key = self.va_list_ptr(varargs);
@@ -766,6 +770,149 @@ impl<'tcx, M: Machine<'tcx>> InterpCx<'tcx, M> {
766770
interp_ok(true)
767771
}
768772

773+
/// Validate whether the value and type passed by the caller are compatible with the type
774+
/// requested by the callee. Based on section 7.16.1.1 of the C23 specification.
775+
///
776+
/// The caller requesting a value of a type is valid when that type is compatible with the type
777+
/// provided by the caller (see `validate_c_variadic_compatible_ty`) and, if both types are
778+
/// integers of different signedness, the passed value must be representable in both types.
779+
fn validate_c_variadic_compatible(
780+
&mut self,
781+
arg_mplace: &MPlaceTy<'tcx, M::Provenance>,
782+
callee_type: TyAndLayout<'tcx>,
783+
) -> InterpResult<'tcx> {
784+
// Identical types are clearly compatible.
785+
if arg_mplace.layout.ty == callee_type.ty {
786+
return interp_ok(());
787+
}
788+
789+
// Types of different sizes can never be compatible.
790+
if arg_mplace.layout.size != callee_type.size {
791+
throw_ub_format!(
792+
"va_arg type mismatch: requested `{}` is incompatible with next argument of type `{}`",
793+
callee_type.ty,
794+
arg_mplace.layout.ty,
795+
)
796+
}
797+
798+
fn validate_cast<'tcx, T, U>(x: T, target_ty: Ty<'tcx>) -> InterpResult<'tcx>
799+
where
800+
T: std::fmt::Display + Copy,
801+
U: std::fmt::Display + TryFrom<T>,
802+
{
803+
if U::try_from(x).is_ok() {
804+
return interp_ok(());
805+
}
806+
807+
throw_ub_format!(
808+
"va_arg value mismatch: value `{x}` cannot be represented by type {target_ty}",
809+
)
810+
}
811+
812+
match self.validate_c_variadic_compatible_ty(arg_mplace.layout.ty, callee_type.ty) {
813+
VarArgCompatible::Identical => interp_ok(()),
814+
VarArgCompatible::ValidPtrCast => interp_ok(()),
815+
VarArgCompatible::Incompatible => throw_ub_format!(
816+
"va_arg type mismatch: requested `{}` is incompatible with next argument of type `{}`",
817+
callee_type.ty,
818+
arg_mplace.layout.ty,
819+
),
820+
VarArgCompatible::CastSignedToUnsigned(int_ty) => {
821+
// Check that the value can be represented in the target type.
822+
let callee_ty = callee_type.ty;
823+
let scalar = self.read_scalar(arg_mplace)?;
824+
match int_ty {
825+
IntTy::Isize => match self.data_layout().pointer_size().bits() {
826+
16 => validate_cast::<i16, u16>(scalar.to_i16()?, callee_ty)?,
827+
32 => validate_cast::<i32, u32>(scalar.to_i32()?, callee_ty)?,
828+
64 => validate_cast::<i64, u64>(scalar.to_i64()?, callee_ty)?,
829+
_ => unreachable!("unexpected pointer size"),
830+
},
831+
IntTy::I8 => validate_cast::<i8, u8>(scalar.to_i8()?, callee_ty)?,
832+
IntTy::I16 => validate_cast::<i16, u16>(scalar.to_i16()?, callee_ty)?,
833+
IntTy::I32 => validate_cast::<i32, u32>(scalar.to_i32()?, callee_ty)?,
834+
IntTy::I64 => validate_cast::<i64, u64>(scalar.to_i64()?, callee_ty)?,
835+
IntTy::I128 => validate_cast::<i128, u128>(scalar.to_i128()?, callee_ty)?,
836+
};
837+
838+
interp_ok(())
839+
}
840+
VarArgCompatible::CastUnsignedToSigned(uint_ty) => {
841+
// Check that the value can be represented in the target type.
842+
let callee_ty = callee_type.ty;
843+
let scalar = self.read_scalar(arg_mplace)?;
844+
match uint_ty {
845+
UintTy::Usize => match self.data_layout().pointer_size().bits() {
846+
16 => validate_cast::<u16, i16>(scalar.to_u16()?, callee_ty)?,
847+
32 => validate_cast::<u32, i32>(scalar.to_u32()?, callee_ty)?,
848+
64 => validate_cast::<u64, i64>(scalar.to_u64()?, callee_ty)?,
849+
_ => unreachable!("unexpected pointer size"),
850+
},
851+
UintTy::U8 => validate_cast::<u8, i8>(scalar.to_u8()?, callee_ty)?,
852+
UintTy::U16 => validate_cast::<u16, i16>(scalar.to_u16()?, callee_ty)?,
853+
UintTy::U32 => validate_cast::<u32, i32>(scalar.to_u32()?, callee_ty)?,
854+
UintTy::U64 => validate_cast::<u64, i64>(scalar.to_u64()?, callee_ty)?,
855+
UintTy::U128 => validate_cast::<u128, i128>(scalar.to_u128()?, callee_ty)?,
856+
};
857+
858+
interp_ok(())
859+
}
860+
}
861+
}
862+
863+
/// Check whether the caller and callee type are compatible for c-variadic calls. Further
864+
/// validation of the argument value may be needed to detect all UB.
865+
///
866+
/// Types `T` and `U` are compatible when:
867+
///
868+
/// - `T` and `U` are the same type.
869+
/// - `T` is a signed integer and `U` the corresponding unsigned integer,
870+
/// - `T` is an unsigned integer and `U` the corresponding signed integer,
871+
/// - `T` and `U` are both pointers, and their target types are compatible.
872+
/// - `T` is a pointer to `c_void` and `U` is a pointer to `i8` or `u8`.
873+
/// - `T` is a pointer to `i8` or `u8` and `U` is a pointer to `c_void`.
874+
fn validate_c_variadic_compatible_ty(
875+
&mut self,
876+
caller_type: Ty<'tcx>,
877+
callee_type: Ty<'tcx>,
878+
) -> VarArgCompatible {
879+
if caller_type == callee_type {
880+
return VarArgCompatible::Identical;
881+
}
882+
883+
// The signedness of c_char is irrelevant here.
884+
let is_c_char = |ty: Ty<'_>| matches!(ty.kind(), ty::Uint(UintTy::U8) | ty::Int(IntTy::I8));
885+
886+
match (caller_type.kind(), callee_type.kind()) {
887+
(ty::RawPtr(caller_target_ty, _), ty::RawPtr(callee_target_ty, _)) => {
888+
// Accept the cast if one type is pointer to qualified or unqualified void,
889+
// and the other is a pointer to a qualified or unqualified character type.
890+
if caller_target_ty.is_c_void(self.tcx.tcx) && is_c_char(*callee_target_ty) {
891+
return VarArgCompatible::ValidPtrCast;
892+
}
893+
if callee_target_ty.is_c_void(self.tcx.tcx) && is_c_char(*caller_target_ty) {
894+
return VarArgCompatible::ValidPtrCast;
895+
}
896+
897+
// Accept the cast if both types are pointers to qualified or unqualified versions
898+
// of compatible types.
899+
match self.validate_c_variadic_compatible_ty(*caller_target_ty, *callee_target_ty) {
900+
VarArgCompatible::Incompatible => VarArgCompatible::Incompatible,
901+
_ => VarArgCompatible::ValidPtrCast,
902+
}
903+
}
904+
(ty::Int(int_ty), ty::Uint(uint_ty)) => {
905+
assert_eq!(int_ty.bit_width(), uint_ty.bit_width());
906+
VarArgCompatible::CastSignedToUnsigned(*int_ty)
907+
}
908+
(ty::Uint(uint_ty), ty::Int(int_ty)) => {
909+
assert_eq!(uint_ty.bit_width(), int_ty.bit_width());
910+
VarArgCompatible::CastUnsignedToSigned(*uint_ty)
911+
}
912+
_ => VarArgCompatible::Incompatible,
913+
}
914+
}
915+
769916
pub(super) fn eval_nondiverging_intrinsic(
770917
&mut self,
771918
intrinsic: &NonDivergingIntrinsic<'tcx>,

library/core/src/ffi/va_list.rs

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -391,10 +391,24 @@ impl<'f> VaList<'f> {
391391
/// # Safety
392392
///
393393
/// 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`.
394+
/// argument is a properly initialized value compatible with the type `T`.
395395
///
396396
/// Calling this function with an incompatible type, an invalid value, or when there
397397
/// are no more variable arguments, is unsound.
398+
///
399+
/// Types `T` and `U` are compatible when:
400+
///
401+
/// - `T` and `U` are the same type.
402+
/// - `T` is a signed integer and `U` the corresponding unsigned integer,
403+
/// - `T` is an unsigned integer and `U` the corresponding signed integer,
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`].
406+
/// - `T` is a pointer to [`i8]` or [`u8`] and `U` is a pointer to [`c_void`].
407+
///
408+
/// When `T` and `U` are both integer types, the value that the caller passes must be
409+
/// representable by `U`.
410+
///
411+
/// [`c_void`]: core::ffi::c_void
398412
#[inline] // Avoid codegen when not used to help backends that don't support VaList.
399413
#[rustc_const_unstable(feature = "const_c_variadic", issue = "151787")]
400414
pub const unsafe fn next_arg<T: VaArgSafe>(&mut self) -> T {

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

Lines changed: 39 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,48 @@ 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+
const { read_as::<u32>(-1i32) };
55+
//~^ ERROR va_arg value mismatch: value `-1` cannot be represented by type u32
56+
const { read_as::<i32>(u32::MAX) };
57+
//~^ ERROR va_arg value mismatch: value `4294967295` cannot be represented by type i32
5558

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

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

62-
const { read_as::<*const u8>(1i32) };
63-
//~^ ERROR va_arg type mismatch: requested `*const u8`, but next argument is `i32`
66+
unsafe fn read_cast_pointer() {
67+
// A pointer mutability cast is OK.
68+
const { read_as::<*const i32>(std::ptr::dangling_mut::<i32>()) };
69+
const { read_as::<*mut i32>(std::ptr::dangling::<i32>()) };
70+
71+
// A pointer cast is OK between compatible types.
72+
const { read_as::<*const i32>(std::ptr::dangling::<u32>()) };
73+
const { read_as::<*const i32>(std::ptr::dangling_mut::<u32>()) };
74+
const { read_as::<*mut i32>(std::ptr::dangling::<u32>()) };
75+
const { read_as::<*mut i32>(std::ptr::dangling_mut::<u32>()) };
76+
77+
// Casting between pointers to i8/u8 and c_void is OK.
78+
const { read_as::<*const c_char>(std::ptr::dangling::<c_void>()) };
79+
const { read_as::<*const c_void>(std::ptr::dangling::<c_char>()) };
80+
const { read_as::<*const i8>(std::ptr::dangling::<c_void>()) };
81+
const { read_as::<*const c_void>(std::ptr::dangling::<i8>()) };
82+
const { read_as::<*const u8>(std::ptr::dangling::<c_void>()) };
83+
const { read_as::<*const c_void>(std::ptr::dangling::<u8>()) };
84+
85+
const { read_as::<*const u16>(std::ptr::dangling::<c_void>()) };
86+
//~^ ERROR va_arg type mismatch: requested `*const u16` is incompatible with next argument of type `*const c_void`
87+
const { read_as::<*const c_void>(std::ptr::dangling::<u16>()) };
88+
//~^ ERROR va_arg type mismatch: requested `*const c_void` is incompatible with next argument of type `*const u16`
89+
90+
const { read_as::<*const u8>(1usize) };
91+
//~^ ERROR requested `*const u8` is incompatible with next argument of type `usize`
6492
}
6593

6694
fn use_after_free() {
@@ -138,7 +166,8 @@ fn drop_of_invalid() {
138166
fn main() {
139167
unsafe {
140168
read_too_many();
141-
read_cast();
169+
read_cast_numeric();
170+
read_cast_pointer();
142171
manual_copy_read();
143172
manual_copy_drop();
144173
manual_copy_forget();

0 commit comments

Comments
 (0)