Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions compiler/rustc_hir_typeck/src/fn_ctxt/checks.rs
Original file line number Diff line number Diff line change
Expand Up @@ -492,6 +492,9 @@ impl<'a, 'tcx> FnCtxt<'a, 'tcx> {
// Make sure we've checked this expr at least once.
let arg_ty = self.check_expr(arg);

// FIXME: Remove this redundant check once we turn the
// `invalid_c_variadic_arguments` FCW into a hard error.

// If the function is c-style variadic, we skipped a bunch of arguments
// so we need to check those, and write out the types
// Ideally this would be folded into the above, for uniform style
Expand Down
133 changes: 131 additions & 2 deletions compiler/rustc_lint/src/builtin.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,8 @@ use rustc_trait_selection::traits::misc::type_allowed_to_implement_copy;
use rustc_trait_selection::traits::query::evaluate_obligation::InferCtxtExt as _;

use crate::diagnostics::{
BuiltinAnonymousParams, BuiltinConstNoMangle, BuiltinDerefNullptr, BuiltinDoubleNegations,
BuiltinDoubleNegationsAddParens, BuiltinEllipsisInclusiveRangePatterns,
BuiltinAnonymousParams, BuiltinCVariadicArgument, BuiltinConstNoMangle, BuiltinDerefNullptr,
BuiltinDoubleNegations, BuiltinDoubleNegationsAddParens, BuiltinEllipsisInclusiveRangePatterns,
BuiltinEllipsisInclusiveRangePatternsLint, BuiltinExplicitOutlives,
BuiltinExplicitOutlivesSuggestion, BuiltinFeatureIssueNote, BuiltinIncompleteFeatures,
BuiltinIncompleteFeaturesHelp, BuiltinInternalFeatures, BuiltinKeywordIdents,
Expand Down Expand Up @@ -3179,3 +3179,132 @@ impl<'tcx> LateLintPass<'tcx> for InternalEqTraitMethodImpls {
}
}
}

// Once we turn this into a hard error, we should delete the redundant check from
// `rustc_hir_typeck::fn_ctxt::FnCtxt::check_argument_types`.
// Turning this into a hard error should consist of adding a trait obligation
// in the type-checking of function arguments.
declare_lint! {
/// The `invalid_c_variadic_arguments` lint detects when a value of
/// an unsupported type is passed as a C-variadic argument (varargs).
///
/// ### Example
///
/// ```rust
/// unsafe extern "C" fn variadic(_: ...) {}
///
/// pub fn foo<T>(x: T) {
/// unsafe {
/// variadic(x);
/// }
/// }
/// ```
///
/// {{produces}}
///
/// ### Explanation
///
/// Only certain types are supported in C-variadic arguments (varargs).
/// In particular, only types that implement the `core::ffi::VaArgSafe`
/// trait are supported.
///
/// Using unsupported types causes undefined behavior. However, the compiler
/// previously didn't consistently check to prevent this from happening in
/// all cases.
///
/// Currently, this lint does not warn on references to `Sized` types, despite
/// the fact that they (unlike raw pointers) don't implement `VaArgSafe`.
/// This is because we might decide to officially support them in the future,
/// by making them implement `VaArgSafe`, and there is too much existing code
/// that passes references as varargs.
///
/// If you encounter this lint in a generic context which will be instantiated
/// only with supported types, consider adding a trait bound such as
/// `T: VaArgSafe`.
///
/// This is a [future-incompatible] lint to transition this to a hard
/// error in the future. See [issue #162483] for more details.
///
/// [issue #162483]: https://github.com/rust-lang/rust/issues/162483
pub INVALID_C_VARIADIC_ARGUMENTS,
Warn,
"arguments passed as C variadic arguments that don't implement `VaArgSafe`",
@future_incompatible = FutureIncompatibleInfo {
reason: fcw!(FutureReleaseError #162483),
};
Comment on lines +3232 to +3234

@RalfJung RalfJung Sep 8, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, people changed the syntax of this macro again and now one cannot easily tell whether this will be reported in dependencies or not. :/

That's a side-effect of #141936. @WaffleLapkin why is report_in_depds an optional field? The default is far from obvious. (When I introduced FutureReleaseErrorDontReportInDeps many people were surprised that FCW do not report-in-deps by default. That's why I introduced this name that makes it so obvious. IMO it is a step backwards that now we again have syntax where this is not obvious.)

View changes since the review

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Based on this, it seems that the field being optional is intentional:

/// If set to `true`, this will make future incompatibility warnings show up in cargo's
/// reports.
///
/// When a future incompatibility warning is first inroduced, set this to `false`
/// (or, rather, don't override the default). This allows crate developers an opportunity
/// to fix the warning before blasting all dependents with a warning they can't fix
/// (dependents have to wait for a new release of the affected crate to be published).
///
/// After a lint has been in this state for a while, consider setting this to true, so it
/// warns for everyone. It is a good signal that it is ready if you can determine that all
/// or most affected crates on crates.io have been updated.
pub report_in_deps: bool,

@RalfJung RalfJung Sep 8, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that's a bad choice. It certainly could have warranted a bit more discussion, given that this effectively reverted changes I made previously (#116049), in terms of what is and is not explicit in the API.

I guess people weren't aware of the prior discussion and didn't realize the downsides of the new API choice. Time to make another PR to make report_in_depds mandatory I guess... except I don't know how to make it mandatory just for FutureReleaseError; we don't need it mandatory for edition errors as those "obviously" are not reported in dependencies. That's the downside of the new structure...

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was indeed not aware of the previous discussion, ugh =_=

I think the justification from #141936, "It gets especially unruly if you want to add non-FutureReleaseError* warnings which are included in the reports." was targeted at EditionAndFutureReleaseError which I was working with at the time, in the process of stabilizing never.

Looking at the current structure, I'd say we can put report_in_deps in ReleaseFcw. That adds the assumption that we only want to report warnings in dependencies if we plan to change something in a future release, but I guess that's fine (and is at the very least currently true).

I'll make a PR for this.

@RalfJung RalfJung Sep 8, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking at the current structure, I'd say we can put report_in_deps in ReleaseFcw. That adds the assumption that we only want to report warnings in dependencies if we plan to change something in a future release, but I guess that's fine (and is at the very least currently true).

I was assuming you'd not want that since it seems to partially revert your PR #141936, by coupling report_in_deps with the reason again. But it sounds great to me so if you can also live with it, all good. :)

I'll make a PR for this.

❤️

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Presumably we'd then add ReleaseFcw to EditionAndFutureReleaseError and to EditionAndFutureReleaseSemanticsChange? (Currently these only include EditionFcw.)

If helpful to factoring, note that lang has been following the policy of setting report_in_deps = true exactly when we make an FCW deny-by-default (and otherwise setting report_in_deps = false). Possibly, after cleaning up any lingering exceptions, it could be OK to lean on that.

@RalfJung RalfJung Sep 9, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

#159700 is a recent example of a Warn + report_in_deps lint. It also shows up in a lot of dependency trees so maybe that was not a good call and it should have been Warn-only for a while like normal FCWs...

}

declare_lint_pass!(InvalidCVariadicArguments => [INVALID_C_VARIADIC_ARGUMENTS]);

impl<'tcx> LateLintPass<'tcx> for InvalidCVariadicArguments {
fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx hir::Expr<'tcx>) {
let (fn_sig, args, is_method_syntax) = match expr.kind {
hir::ExprKind::Call(f, args) => {
let fn_ty = cx.typeck_results().expr_ty_adjusted(f);
if !matches!(fn_ty.kind(), ty::FnPtr(_, _) | ty::FnDef(_, _)) {
// The call expression is done via one of the Fn traits.
// Those don't support C variadics, so nothing to lint here.
return;
}
(fn_ty.fn_sig(cx.tcx), args, false)
}
hir::ExprKind::MethodCall(_, _, args, _) => {
// This can be `None` if the receiver has a error in type-checking.
// For example: `tests/ui/consts/const-eval/infinite_loop.rs`
let Some(method_def) = cx.typeck_results().type_dependent_def_id(expr.hir_id)
else {
cx.tcx.dcx().span_delayed_bug(expr.span, "method should have a DefId");
return;
};
(cx.tcx.fn_sig(method_def).skip_binder(), args, true)
}
_ => {
return;
}
};

if !fn_sig.c_variadic() {
return;
}
let num_args = fn_sig.inputs().skip_binder().len();
// num_args includes the method receiver
let arg_offset = if is_method_syntax { num_args.strict_sub(1) } else { num_args };
let Some(va_arg_safe) = cx.tcx.lang_items().get(LangItem::VaArgSafe) else {
return;
};

for arg in &args[arg_offset..] {
let arg_ty = cx.typeck_results().expr_ty_adjusted(arg);
if cx
.tcx
.infer_ctxt()
.build(cx.typing_mode())
.type_implements_trait(va_arg_safe, [arg_ty], cx.param_env)
.must_apply_modulo_regions()
{
continue;
}
// Thin references technically do not implement `VaArgSafe`.
// However, we might make them implement `VaArgSafe` later,
// so, do not lint such arguments.
if let ty::Ref(_, referent_ty, _) = arg_ty.kind()
&& referent_ty.is_sized(cx.tcx, cx.typing_env())
{
continue;
}
Comment thread
theemathas marked this conversation as resolved.
Comment thread
RalfJung marked this conversation as resolved.
// TODO Remove this before merging. This is for crater only.
#[allow(rustc::symbol_intern_string_literal)]
if cx.tcx.sess.config.contains(&(Symbol::intern("crater_hack"), None)) {
cx.tcx.dcx().span_err(
arg.span,
format!("CRATER ERROR: C-variadic argument with type `{}`.", arg_ty),
);
}
cx.emit_span_lint(
INVALID_C_VARIADIC_ARGUMENTS,
arg.span,
BuiltinCVariadicArgument { arg_ty },
);
}
}
}
7 changes: 7 additions & 0 deletions compiler/rustc_lint/src/diagnostics.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3219,3 +3219,10 @@ pub(crate) enum RawBorrowViaReferenceSuggestion<'a> {
#[help("consider using `&raw {$mutbl}` for a safer and more explicit raw pointer")]
Spanless { mutbl: &'a str },
}

#[derive(Diagnostic)]
#[diag("type `{$arg_ty}` does not implement `VaArgSafe`")]
#[note("values passed as C-variadic arguments must implement `core::ffi::VaArgSafe`")]
pub(crate) struct BuiltinCVariadicArgument<'tcx> {
pub arg_ty: Ty<'tcx>,
}
1 change: 1 addition & 0 deletions compiler/rustc_lint/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,7 @@ late_lint_methods!(
InteriorMutableConsts: InteriorMutableConsts,
InternalEqTraitMethodImpls: InternalEqTraitMethodImpls,
InvalidAtomicOrdering: InvalidAtomicOrdering,
InvalidCVariadicArguments: InvalidCVariadicArguments,
InvalidFromUtf8: InvalidFromUtf8,
InvalidNoMangleItems: InvalidNoMangleItems,
InvalidReferenceCasting: InvalidReferenceCasting,
Expand Down
1 change: 1 addition & 0 deletions src/tools/miri/tests/fail/c-variadic-ignored-argument.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
#![expect(invalid_c_variadic_arguments)]
// While 1-ZST are currently ignored on most ABIs, we don't guarantee that, and it's UB to
// rely on it.

Expand Down
4 changes: 3 additions & 1 deletion tests/ui/abi/mir/mir_codegen_calls_variadic.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
//@ run-pass

use std::ffi::VaArgSafe;

#[link(name = "rust_test_helpers", kind = "static")]
extern "C" {
fn rust_interesting_average(_: i64, ...) -> f64;
}

fn test<T, U>(a: i64, b: i64, c: i64, d: i64, e: i64, f: T, g: U) -> i64 {
fn test<T: VaArgSafe, U: VaArgSafe>(a: i64, b: i64, c: i64, d: i64, e: i64, f: T, g: U) -> i64 {
unsafe {
rust_interesting_average(
6, a, a as f64, b, b as f64, c, c as f64, d, d as f64, e, e as f64, f, g,
Expand Down
1 change: 1 addition & 0 deletions tests/ui/consts/const-eval/c-variadic-ignored-argument.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
#![feature(const_c_variadic)]
#![feature(const_destruct)]
#![crate_type = "lib"]
#![expect(invalid_c_variadic_arguments)]

// Regression test for when a c-variadic argument is `PassMode::Ignore`. The caller won't pass the
// argument, but the callee ABI does have the argument. Ensure that const-eval is able to handle
Expand Down
169 changes: 169 additions & 0 deletions tests/ui/lint/invalid_c_variadic_arguments.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
//@ check-pass

use std::ffi::VaArgSafe;

unsafe extern "C" fn variadic(_: ...) {}

fn main() {
unsafe {
variadic();
variadic(1_i32);
variadic(String::new());
//~^ WARN type `String` does not implement `VaArgSafe`
//~| WARN this was previously accepted by the compiler but is being phased out
variadic(1_i32, String::new());
//~^ WARN type `String` does not implement `VaArgSafe`
//~| WARN this was previously accepted by the compiler but is being phased out
variadic(String::new(), 1_i32);
//~^ WARN type `String` does not implement `VaArgSafe`
//~| WARN this was previously accepted by the compiler but is being phased out
variadic(String::new(), String::new());
//~^ WARN type `String` does not implement `VaArgSafe`
//~| WARN this was previously accepted by the compiler but is being phased out
//~| WARN type `String` does not implement `VaArgSafe`
//~| WARN this was previously accepted by the compiler but is being phased out
}
}

fn generic<T>(x: T) {
unsafe {
variadic(x);
//~^ WARN type `T` does not implement `VaArgSafe`
//~| WARN this was previously accepted by the compiler but is being phased out
}
}

fn generic_with_bound<T: VaArgSafe>(x: T) {
unsafe {
variadic(x);
}
}

fn indirect() {
unsafe {
let f = variadic;
f(String::new());
//~^ WARN type `String` does not implement `VaArgSafe`
//~| WARN this was previously accepted by the compiler but is being phased out
let f_ref = &f;
f_ref(String::new());
//~^ WARN type `String` does not implement `VaArgSafe`
//~| WARN this was previously accepted by the compiler but is being phased out
let g = variadic as unsafe extern "C" fn(...);
g(String::new());
//~^ WARN type `String` does not implement `VaArgSafe`
//~| WARN this was previously accepted by the compiler but is being phased out
let g_ref = &g;
g_ref(String::new());
//~^ WARN type `String` does not implement `VaArgSafe`
//~| WARN this was previously accepted by the compiler but is being phased out
}
}

#[repr(C)]
#[derive(Clone, Copy)]
struct MyStruct {
x: i32,
y: i32,
}

unsafe extern "C" fn variadic_after_struct(_: MyStruct, _: ...) {}

fn simple_variadic_after_struct(my_struct: MyStruct) {
unsafe {
variadic_after_struct(my_struct);
variadic_after_struct(my_struct, 1_i32);
variadic_after_struct(my_struct, String::new());
//~^ WARN type `String` does not implement `VaArgSafe`
//~| WARN this was previously accepted by the compiler but is being phased out
variadic_after_struct(my_struct, 1_i32, String::new());
//~^ WARN type `String` does not implement `VaArgSafe`
//~| WARN this was previously accepted by the compiler but is being phased out
variadic_after_struct(my_struct, String::new(), 1_i32);
//~^ WARN type `String` does not implement `VaArgSafe`
//~| WARN this was previously accepted by the compiler but is being phased out
variadic_after_struct(my_struct, String::new(), String::new());
//~^ WARN type `String` does not implement `VaArgSafe`
//~| WARN this was previously accepted by the compiler but is being phased out
//~| WARN type `String` does not implement `VaArgSafe`
//~| WARN this was previously accepted by the compiler but is being phased out
}
}

trait Trait {
type Assoc<'a>;
}

// Unlikely case which our lint doesn't catch.
fn lifetime_dependent<'a, 'b, T: Trait<Assoc<'a>: VaArgSafe>>(x: <T as Trait>::Assoc<'b>) {
unsafe {
variadic(x);
}
}

// We don't lint (thin) references even though they currently don't implement VaArgSafe
fn references<T, U: ?Sized>(tr: &T, tm: &mut T, ur: &U, um: &mut U) {
unsafe {
variadic(&String::new());
variadic(&mut String::new());
variadic(&String::new() as &dyn Send);
//~^ WARN type `&dyn Send` does not implement `VaArgSafe`
//~| WARN this was previously accepted by the compiler but is being phased out
variadic(&mut String::new() as &mut dyn Send);
//~^ WARN type `&mut dyn Send` does not implement `VaArgSafe`
//~| WARN this was previously accepted by the compiler but is being phased out
variadic(tr);
variadic(tm);
variadic(ur);
//~^ WARN type `&U` does not implement `VaArgSafe`
//~| WARN this was previously accepted by the compiler but is being phased out
variadic(um);
//~^ WARN type `&mut U` does not implement `VaArgSafe`
//~| WARN this was previously accepted by the compiler but is being phased out
}
}

// Quirk with our current hard error: It allows infer vars as varargs
// even if they wouldn't be allowed when the concrete type is known.
fn infer_var() {
unsafe {
let mut x = 1;
variadic(x);
//~^ WARN type `u8` does not implement `VaArgSafe`
//~| WARN this was previously accepted by the compiler but is being phased out
x = 1_u8;
}
}

fn integer_float_fallback() {
unsafe {
variadic(1);
variadic(1.0);
}
}

struct Thing;
impl Thing {
unsafe extern "C" fn variadic_method(&self, _: ...) {}
}

fn method_call_syntax() {
unsafe {
Thing.variadic_method();
Thing.variadic_method(1_i32);
Thing.variadic_method(String::new());
//~^ WARN type `String` does not implement `VaArgSafe`
//~| WARN this was previously accepted by the compiler but is being phased out
Thing.variadic_method(1_i32, String::new());
//~^ WARN type `String` does not implement `VaArgSafe`
//~| WARN this was previously accepted by the compiler but is being phased out
Thing.variadic_method(String::new(), 1_i32);
//~^ WARN type `String` does not implement `VaArgSafe`
//~| WARN this was previously accepted by the compiler but is being phased out
Thing.variadic_method(String::new(), String::new());
//~^ WARN type `String` does not implement `VaArgSafe`
//~| WARN this was previously accepted by the compiler but is being phased out
//~| WARN type `String` does not implement `VaArgSafe`
//~| WARN this was previously accepted by the compiler but is being phased out
}
}
Loading
Loading