Skip to content

Commit ec80a09

Browse files
committed
ImproperCTypes: change handling of ADTs
A change in how type checking goes through structs/enums/unions, mostly to be able to yield multiple lints if multiple fields are unsafe
1 parent f043685 commit ec80a09

17 files changed

+303
-121
lines changed

compiler/rustc_lint/messages.ftl

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -377,24 +377,25 @@ lint_improper_ctypes_slice_reason = slices have no C equivalent
377377
378378
lint_improper_ctypes_str_help = consider using `*const u8` and a length instead
379379
lint_improper_ctypes_str_reason = string slices have no C equivalent
380-
lint_improper_ctypes_struct_fieldless_help = consider adding a member to this struct
381380
382-
lint_improper_ctypes_struct_fieldless_reason = this struct has no fields
381+
lint_improper_ctypes_struct_consider_transparent = `{$ty}` has exactly one non-zero-sized field, consider making it `#[repr(transparent)]` instead
383382
lint_improper_ctypes_struct_dueto = this struct/enum/union (`{$ty}`) is FFI-unsafe due to a `{$inner_ty}` field
384383
385-
lint_improper_ctypes_struct_layout_help = consider adding a `#[repr(C)]` or `#[repr(transparent)]` attribute to this struct
384+
lint_improper_ctypes_struct_fieldless_help = consider adding a member to this struct
385+
lint_improper_ctypes_struct_fieldless_reason = `{$ty}` has no fields
386386
387-
lint_improper_ctypes_struct_layout_reason = this struct has unspecified layout
388-
lint_improper_ctypes_struct_non_exhaustive = this struct is non-exhaustive
389-
lint_improper_ctypes_struct_zst = this struct contains only zero-sized fields
387+
lint_improper_ctypes_struct_layout_help = consider adding a `#[repr(C)]` (not `#[repr(C,packed)]`) or `#[repr(transparent)]` attribute to `{$ty}`
388+
lint_improper_ctypes_struct_layout_reason = `{$ty}` has unspecified layout
389+
lint_improper_ctypes_struct_non_exhaustive = `{$ty}` is non-exhaustive
390+
lint_improper_ctypes_struct_zst = `{$ty}` contains only zero-sized fields
390391
391392
lint_improper_ctypes_tuple_help = consider using a struct instead
392-
393393
lint_improper_ctypes_tuple_reason = tuples have unspecified layout
394-
lint_improper_ctypes_union_fieldless_help = consider adding a member to this union
395394
395+
lint_improper_ctypes_union_consider_transparent = `{$ty}` has exactly one non-zero-sized field, consider making it `#[repr(transparent)]` instead
396+
lint_improper_ctypes_union_fieldless_help = consider adding a member to this union
396397
lint_improper_ctypes_union_fieldless_reason = this union has no fields
397-
lint_improper_ctypes_union_layout_help = consider adding a `#[repr(C)]` or `#[repr(transparent)]` attribute to this union
398+
lint_improper_ctypes_union_layout_help = consider adding a `#[repr(C)]` attribute to this union
398399
399400
lint_improper_ctypes_union_layout_reason = this union has unspecified layout
400401
lint_improper_ctypes_union_non_exhaustive = this union is non-exhaustive

compiler/rustc_lint/src/types.rs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -707,6 +707,26 @@ pub(crate) fn transparent_newtype_field<'a, 'tcx>(
707707
})
708708
}
709709

710+
/// for a given ADT variant, list which fields are non-1ZST
711+
/// (`repr(transparent)` guarantees that there is at most one)
712+
pub(crate) fn map_non_1zst_fields<'a, 'tcx>(
713+
tcx: TyCtxt<'tcx>,
714+
variant: &'a ty::VariantDef,
715+
) -> Vec<bool> {
716+
let typing_env = ty::TypingEnv::non_body_analysis(tcx, variant.def_id);
717+
variant
718+
.fields
719+
.iter()
720+
.map(|field| {
721+
let field_ty = tcx.type_of(field.did).instantiate_identity();
722+
let is_1zst = tcx
723+
.layout_of(typing_env.as_query_input(field_ty))
724+
.is_ok_and(|layout| layout.is_1zst());
725+
!is_1zst
726+
})
727+
.collect()
728+
}
729+
710730
/// Is type known to be non-null?
711731
fn ty_is_known_nonnull<'tcx>(
712732
tcx: TyCtxt<'tcx>,

compiler/rustc_lint/src/types/improper_ctypes.rs

Lines changed: 143 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -177,37 +177,6 @@ fn get_type_from_field<'tcx>(
177177
cx.tcx.try_normalize_erasing_regions(cx.typing_env(), field_ty).unwrap_or(field_ty)
178178
}
179179

180-
/// Check a variant of a non-exhaustive enum for improper ctypes
181-
///
182-
/// We treat `#[non_exhaustive] enum` as "ensure that code will compile if new variants are added".
183-
/// This includes linting, on a best-effort basis. There are valid additions that are unlikely.
184-
///
185-
/// Adding a data-carrying variant to an existing C-like enum that is passed to C is "unlikely",
186-
/// so we don't need the lint to account for it.
187-
/// e.g. going from enum Foo { A, B, C } to enum Foo { A, B, C, D(u32) }.
188-
pub(crate) fn check_non_exhaustive_variant(
189-
non_exhaustive_variant_list: bool,
190-
variant: &ty::VariantDef,
191-
) -> ControlFlow<DiagMessage, ()> {
192-
// non_exhaustive suggests it is possible that someone might break ABI
193-
// see: https://github.com/rust-lang/rust/issues/44109#issuecomment-537583344
194-
// so warn on complex enums being used outside their crate
195-
if non_exhaustive_variant_list {
196-
// which is why we only warn about really_tagged_union reprs from https://rust.tf/rfc2195
197-
// with an enum like `#[repr(u8)] enum Enum { A(DataA), B(DataB), }`
198-
// but exempt enums with unit ctors like C's (e.g. from rust-bindgen)
199-
if variant_has_complex_ctor(variant) {
200-
return ControlFlow::Break(fluent::lint_improper_ctypes_non_exhaustive);
201-
}
202-
}
203-
204-
if variant.field_list_has_applicable_non_exhaustive() {
205-
return ControlFlow::Break(fluent::lint_improper_ctypes_non_exhaustive_variant);
206-
}
207-
208-
ControlFlow::Continue(())
209-
}
210-
211180
fn variant_has_complex_ctor(variant: &ty::VariantDef) -> bool {
212181
// CtorKind::Const means a "unit" ctor
213182
!matches!(variant.ctor_kind(), Some(CtorKind::Const))
@@ -378,7 +347,6 @@ impl<'tcx> FfiResult<'tcx> {
378347
}
379348
/// If the FfiPhantom variant, turns it into a FfiUnsafe version.
380349
/// Otherwise, keep unchanged.
381-
#[expect(unused)]
382350
fn forbid_phantom(self) -> Self {
383351
match self {
384352
Self::FfiPhantom(ty) => {
@@ -960,34 +928,113 @@ impl<'a, 'tcx> ImproperCTypesVisitor<'a, 'tcx> {
960928
) -> FfiResult<'tcx> {
961929
use FfiResult::*;
962930

963-
let transparent_with_all_zst_fields = if def.repr().transparent() {
964-
if let Some(field) = super::transparent_newtype_field(self.cx.tcx, variant) {
965-
// Transparent newtypes have at most one non-ZST field which needs to be checked..
966-
let field_ty = get_type_from_field(self.cx, field, args);
967-
return self.visit_type(state.get_next(ty), field_ty);
931+
let mut ffires_accumulator = FfiSafe;
932+
933+
let (transparent_with_all_zst_fields, field_list) =
934+
if !matches!(def.adt_kind(), AdtKind::Enum) && def.repr().transparent() {
935+
// determine if there is 0 or 1 non-1ZST field, and which it is.
936+
// (note: for enums, "transparent" means 1-variant)
937+
if let Some(field) = super::transparent_newtype_field(self.cx.tcx, variant) {
938+
// Transparent newtypes have at most one non-ZST field which needs to be checked later
939+
(false, vec![field])
940+
} else {
941+
// ..or have only ZST fields, which is FFI-unsafe (unless those fields are all
942+
// `PhantomData`).
943+
(true, variant.fields.iter().collect::<Vec<_>>())
944+
}
968945
} else {
969-
// ..or have only ZST fields, which is FFI-unsafe (unless those fields are all
970-
// `PhantomData`).
971-
true
972-
}
973-
} else {
974-
false
975-
};
946+
(false, variant.fields.iter().collect::<Vec<_>>())
947+
};
976948

977949
// We can't completely trust `repr(C)` markings, so make sure the fields are actually safe.
978950
let mut all_phantom = !variant.fields.is_empty();
979-
for field in &variant.fields {
951+
let mut fields_ok_list = vec![true; field_list.len()];
952+
953+
for (field_i, field) in field_list.into_iter().enumerate() {
980954
let field_ty = get_type_from_field(self.cx, field, args);
981-
all_phantom &= match self.visit_type(state.get_next(ty), field_ty) {
982-
FfiSafe => false,
955+
let ffi_res = self.visit_type(state.get_next(ty), field_ty);
956+
957+
// checking that this is not an FfiUnsafe due to an unit type:
958+
// visit_type should be smart enough to not consider it unsafe if called from another ADT
959+
#[cfg(debug_assertions)]
960+
if let FfiUnsafe(ref reasons) = ffi_res {
961+
if let (1, Some(FfiUnsafeExplanation { reason, .. })) =
962+
(reasons.len(), reasons.first())
963+
{
964+
let FfiUnsafeReason { ty, .. } = reason.as_ref();
965+
debug_assert!(!ty.is_unit());
966+
}
967+
}
968+
969+
all_phantom &= match ffi_res {
983970
FfiPhantom(..) => true,
971+
FfiSafe => false,
984972
r @ FfiUnsafe { .. } => {
985-
return r.wrap_all(ty, fluent::lint_improper_ctypes_struct_dueto, None);
973+
fields_ok_list[field_i] = false;
974+
ffires_accumulator += r;
975+
false
986976
}
987977
}
988978
}
989979

990-
if all_phantom {
980+
// if we have bad fields, also report a possible transparent_with_all_zst_fields
981+
// (if this combination is somehow possible)
982+
// otherwise, having all fields be phantoms
983+
// takes priority over transparent_with_all_zst_fields
984+
if let FfiUnsafe(explanations) = ffires_accumulator {
985+
debug_assert!(def.repr().c() || def.repr().transparent() || def.repr().int.is_some());
986+
987+
if def.repr().transparent() || matches!(def.adt_kind(), AdtKind::Enum) {
988+
let field_ffires = FfiUnsafe(explanations).wrap_all(
989+
ty,
990+
fluent::lint_improper_ctypes_struct_dueto,
991+
None,
992+
);
993+
if transparent_with_all_zst_fields {
994+
field_ffires
995+
+ FfiResult::new_with_reason(
996+
ty,
997+
fluent::lint_improper_ctypes_struct_zst,
998+
None,
999+
)
1000+
} else {
1001+
field_ffires
1002+
}
1003+
} else {
1004+
// since we have a repr(C) struct/union, there's a chance that we have some unsafe fields,
1005+
// but also exactly one non-1ZST field that is FFI-safe:
1006+
// we want to suggest repr(transparent) here.
1007+
// (FIXME(ctypes): confirm that this makes sense for unions once #60405 / RFC2645 stabilises)
1008+
let non_1zst_fields = super::map_non_1zst_fields(self.cx.tcx, variant);
1009+
let (last_non_1zst, non_1zst_count) = non_1zst_fields.into_iter().enumerate().fold(
1010+
(None, 0_usize),
1011+
|(prev_nz, count), (field_i, is_nz)| {
1012+
if is_nz { (Some(field_i), count + 1) } else { (prev_nz, count) }
1013+
},
1014+
);
1015+
let help = if non_1zst_count == 1
1016+
&& last_non_1zst.map(|field_i| fields_ok_list[field_i]) == Some(true)
1017+
{
1018+
match def.adt_kind() {
1019+
AdtKind::Struct => {
1020+
Some(fluent::lint_improper_ctypes_struct_consider_transparent)
1021+
}
1022+
AdtKind::Union => {
1023+
Some(fluent::lint_improper_ctypes_union_consider_transparent)
1024+
}
1025+
AdtKind::Enum => bug!("cannot suggest an enum to be repr(transparent)"),
1026+
}
1027+
} else {
1028+
None
1029+
};
1030+
1031+
FfiUnsafe(explanations).wrap_all(
1032+
ty,
1033+
fluent::lint_improper_ctypes_struct_dueto,
1034+
help,
1035+
)
1036+
}
1037+
} else if all_phantom {
9911038
FfiPhantom(ty)
9921039
} else if transparent_with_all_zst_fields {
9931040
FfiResult::new_with_reason(ty, fluent::lint_improper_ctypes_struct_zst, None)
@@ -1093,24 +1140,58 @@ impl<'a, 'tcx> ImproperCTypesVisitor<'a, 'tcx> {
10931140
// FIXME(ctypes): connect `def.repr().int` to visit_numeric
10941141
// (for now it's OK, `repr(char)` doesn't exist and visit_numeric doesn't warn on anything else)
10951142

1096-
let non_exhaustive = def.variant_list_has_applicable_non_exhaustive();
1143+
let enum_non_exhaustive = def.variant_list_has_applicable_non_exhaustive();
10971144
// Check the contained variants.
1098-
let ret = def.variants().iter().try_for_each(|variant| {
1099-
check_non_exhaustive_variant(non_exhaustive, variant)
1100-
.map_break(|reason| FfiResult::new_with_reason(ty, reason, None))?;
11011145

1102-
match self.visit_variant_fields(state, ty, def, variant, args) {
1103-
FfiSafe => ControlFlow::Continue(()),
1104-
r => ControlFlow::Break(r),
1105-
}
1146+
// non_exhaustive suggests it is possible that someone might break ABI
1147+
// See: https://github.com/rust-lang/rust/issues/44109#issuecomment-537583344
1148+
// so warn on complex enums being used outside their crate.
1149+
//
1150+
// We treat `#[non_exhaustive]` enum variants as unsafe if the enum is passed by-value,
1151+
// as additions it will change it size.
1152+
//
1153+
// We treat `#[non_exhaustive] enum` as "ensure that code will compile if new variants are added".
1154+
// This includes linting, on a best-effort basis. There are valid additions that are unlikely.
1155+
//
1156+
// Adding a data-carrying variant to an existing C-like enum that is passed to C is "unlikely",
1157+
// so we don't need the lint to account for it.
1158+
// e.g. going from enum Foo { A, B, C } to enum Foo { A, B, C, D(u32) }.
1159+
// Which is why we only warn about really_tagged_union reprs from https://rust.tf/rfc2195
1160+
// with an enum like `#[repr(u8)] enum Enum { A(DataA), B(DataB), }`
1161+
// but exempt enums with unit ctors like C's (e.g. from rust-bindgen)
1162+
1163+
let (mut improper_on_nonexhaustive_flag, mut nonexhaustive_variant_flag) = (false, false);
1164+
def.variants().iter().for_each(|variant| {
1165+
improper_on_nonexhaustive_flag |=
1166+
enum_non_exhaustive && variant_has_complex_ctor(variant);
1167+
nonexhaustive_variant_flag |= variant.field_list_has_applicable_non_exhaustive();
11061168
});
1107-
if let ControlFlow::Break(result) = ret {
1169+
1170+
if improper_on_nonexhaustive_flag {
1171+
FfiResult::new_with_reason(ty, fluent::lint_improper_ctypes_non_exhaustive, None)
1172+
} else if nonexhaustive_variant_flag {
1173+
FfiResult::new_with_reason(
1174+
ty,
1175+
fluent::lint_improper_ctypes_non_exhaustive_variant,
1176+
None,
1177+
)
1178+
} else {
1179+
let ffires = def
1180+
.variants()
1181+
.iter()
1182+
.map(|variant| {
1183+
let variant_res = self.visit_variant_fields(state, ty, def, variant, args);
1184+
// FIXME(ctypes): check that enums allow any (up to all) variants to be phantoms?
1185+
// (previous code says no, but I don't know why? the problem with phantoms is that they're ZSTs, right?)
1186+
variant_res.forbid_phantom()
1187+
})
1188+
.reduce(|r1, r2| r1 + r2)
1189+
.unwrap(); // always at least one variant if we hit this branch
1190+
11081191
// this enum is visited in the middle of another lint,
11091192
// so we override the "cause type" of the lint
1110-
// (for more detail, see comment in ``visit_struct_union`` before its call to ``result.with_overrides``)
1111-
result.with_overrides(Some(ty))
1112-
} else {
1113-
FfiSafe
1193+
// (for more detail, see comment in ``visit_struct_union`` before its call to ``ffires.with_overrides``)
1194+
ffires.with_overrides(Some(ty))
11141195
}
11151196
}
11161197

tests/ui/extern/extern-C-str-arg-ice-80125.stderr

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@ warning: `extern` fn uses type `Struct`, which is not FFI-safe
1414
LL | pub extern "C" fn register_something(bind: ExternCallback) -> Struct {
1515
| ^^^^^^ not FFI-safe
1616
|
17-
= help: consider adding a `#[repr(C)]` or `#[repr(transparent)]` attribute to this struct
18-
= note: this struct has unspecified layout
17+
= help: consider adding a `#[repr(C)]` (not `#[repr(C,packed)]`) or `#[repr(transparent)]` attribute to `Struct`
18+
= note: `Struct` has unspecified layout
1919
note: the type is defined here
2020
--> $DIR/extern-C-str-arg-ice-80125.rs:6:1
2121
|

tests/ui/extern/issue-16250.stderr

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@ error: `extern` block uses type `Foo`, which is not FFI-safe
44
LL | pub fn foo(x: (Foo));
55
| ^^^ not FFI-safe
66
|
7-
= help: consider adding a `#[repr(C)]` or `#[repr(transparent)]` attribute to this struct
8-
= note: this struct has unspecified layout
7+
= help: consider adding a `#[repr(C)]` (not `#[repr(C,packed)]`) or `#[repr(transparent)]` attribute to `Foo`
8+
= note: `Foo` has unspecified layout
99
note: the type is defined here
1010
--> $DIR/issue-16250.rs:3:1
1111
|

tests/ui/lint/improper-ctypes/lint-113436-1.stderr

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@ note: the type is defined here
1010
|
1111
LL | pub struct Bar {
1212
| ^^^^^^^^^^^^^^
13-
= help: consider adding a `#[repr(C)]` or `#[repr(transparent)]` attribute to this struct
14-
= note: this struct has unspecified layout
13+
= help: consider adding a `#[repr(C)]` (not `#[repr(C,packed)]`) or `#[repr(transparent)]` attribute to `NotSafe`
14+
= note: `NotSafe` has unspecified layout
1515
note: the type is defined here
1616
--> $DIR/lint-113436-1.rs:13:1
1717
|
@@ -35,8 +35,8 @@ note: the type is defined here
3535
|
3636
LL | pub struct Bar {
3737
| ^^^^^^^^^^^^^^
38-
= help: consider adding a `#[repr(C)]` or `#[repr(transparent)]` attribute to this struct
39-
= note: this struct has unspecified layout
38+
= help: consider adding a `#[repr(C)]` (not `#[repr(C,packed)]`) or `#[repr(transparent)]` attribute to `NotSafe`
39+
= note: `NotSafe` has unspecified layout
4040
note: the type is defined here
4141
--> $DIR/lint-113436-1.rs:13:1
4242
|

tests/ui/lint/improper-ctypes/lint-73249-5.stderr

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,12 @@ error: `extern` block uses type `A`, which is not FFI-safe
44
LL | pub fn lint_me() -> A;
55
| ^ not FFI-safe
66
|
7+
= note: this struct/enum/union (`A`) is FFI-unsafe due to a `Qux` field
8+
note: the type is defined here
9+
--> $DIR/lint-73249-5.rs:16:1
10+
|
11+
LL | pub struct A {
12+
| ^^^^^^^^^^^^
713
= note: opaque types have no C equivalent
814
note: the lint level is defined here
915
--> $DIR/lint-73249-5.rs:2:9

0 commit comments

Comments
 (0)