Skip to content

Commit feebfa2

Browse files
Rollup merge of rust-lang#155299 - RalfJung:repr_transparent_non_zst_fields, r=jdonszelmann
make repr_transparent_non_zst_fields a hard error This lint is about which fields we consider "trivial" for `repr(transparent)`. For `repr(transparent)` to be valid, there can be at most one non-trivial field. In other words, trivial fields are those that we promise do not affect the layout or ABI of the `repr(transparent)` type. Historically we considered all types with size 0 and alignment 1 (i.e., all 1-ZST) to be trivial. However we'd like to take some of that back: - Types might be 1-ZST today but that's not actually meant to be a semver guarantee, so it's bad for downstream code to rely on it. Therefore we should not accept types that have private fields or that are marked `#[non_exhaustive]` (except when we are in the same crate as that type). - Types might be 1-ZST but still be relevant for the ABI because they are `repr(C)` and who knows what the C ABI does. In particular on MSVC a struct whose only field is a 0-length array has size 1. Rust incorrectly gives it size 0. With rust-lang/rfcs#3845 we can hopefully fix the layout, which would make "is that type a 1-ZST" target-dependent, and we ideally should reject such code on all targets. This was a deny-by-default FCW since rust-lang#147185, which landed almost 6 months ago and shipped with Rust 1.93. (If this PR lands now it will ship with 1.97.) Already back then we found hardly any crater impact. The [tracking issue](rust-lang#78586) has had no new relevant backrefs since that PR landed. So, I think it is time to make this a hard error. Fixes rust-lang#78586 (tracking issue) Fixes rust-lang/unsafe-code-guidelines#552 because this means the `repr(transparent)` ABI compatibility rule no longer ever "ignores" `repr(C)` fields. @rust-lang/lang What do you think? See [here](rust-lang#155299 (comment)) for the crater analysis; the summary is that there's no relevant regressions found in the wild. But some points have been raised by people: - The [`ghost`](https://github.com/dtolnay/ghost) crate offers a macro to define `PhantomData`-like types, and those types involve a `repr(C)`. If someone uses such a type as a marker inside a `repr(transparent)`, that will no longer work. Apparently nobody does that in the code checked by crater. A new version of the crate has been released that fixes this. - rust-lang#155925 is unresolved: there is currently no way for a crate to say "yes this type has private field but I promise it will remain a 1-ZST" (except via the unstable `#[rustc_pub_transparent]`). That means it is not possible for a crate to expose a semantically relevant maker type (like `GhostToken`) that is "trivial" for `repr(transparent)` purposes. Apparently currently nobody does this in the ecosystem (the parts crater can see, anyway), but it seems like a sensible thing to do. If we are concerned about this, we could limit this PR to only make the `repr(C)` and `#[non_exhaustive]` part of the check a hard error, and leave the "has private fields" part as a warning. Also note that the private field check is technically a bit odd: we literally check "is the type defined in this crate or are all fields public". We do *not* check if the current module can access those fields. So if a type has private fields then one can rely on it being "trivial" everywhere in the current crate, even outside the module that defined the type. This is not how field privacy usually works. If we want to restrict this to "only modules that can 'see' the fields are allowed to rely on the type being a 1-ZST", that's technically a breaking change. I don't know if this was a deliberate choice or just the easiest thing to implement. @scottmcm do you remember?
2 parents 3130956 + 5e44ddb commit feebfa2

18 files changed

Lines changed: 375 additions & 876 deletions

compiler/rustc_hir_analysis/src/check/check.rs

Lines changed: 101 additions & 105 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ use rustc_hir::def::{CtorKind, DefKind};
1111
use rustc_hir::{LangItem, Node, find_attr, intravisit};
1212
use rustc_infer::infer::{RegionVariableOrigin, TyCtxtInferExt};
1313
use rustc_infer::traits::{Obligation, ObligationCauseCode, WellFormedLoc};
14-
use rustc_lint_defs::builtin::{REPR_TRANSPARENT_NON_ZST_FIELDS, UNSUPPORTED_CALLING_CONVENTIONS};
14+
use rustc_lint_defs::builtin::UNSUPPORTED_CALLING_CONVENTIONS;
1515
use rustc_macros::Diagnostic;
1616
use rustc_middle::hir::nested_filter;
1717
use rustc_middle::middle::resolve_bound_vars::ResolvedArg;
@@ -1735,39 +1735,6 @@ pub(super) fn check_packed_inner(
17351735
}
17361736

17371737
pub(super) fn check_transparent<'tcx>(tcx: TyCtxt<'tcx>, adt: ty::AdtDef<'tcx>) {
1738-
struct ZeroSizedFieldReprTransparentIncompatibility<'tcx> {
1739-
unsuited: UnsuitedInfo<'tcx>,
1740-
}
1741-
1742-
impl<'a, 'tcx> Diagnostic<'a, ()> for ZeroSizedFieldReprTransparentIncompatibility<'tcx> {
1743-
fn into_diag(self, dcx: DiagCtxtHandle<'a>, level: Level) -> Diag<'a, ()> {
1744-
let Self { unsuited } = self;
1745-
let (title, note) = match unsuited.reason {
1746-
UnsuitedReason::NonExhaustive => (
1747-
"external non-exhaustive types",
1748-
"is marked with `#[non_exhaustive]`, so it could become non-zero-sized in the future.",
1749-
),
1750-
UnsuitedReason::PrivateField => (
1751-
"external types with private fields",
1752-
"contains private fields, so it could become non-zero-sized in the future.",
1753-
),
1754-
UnsuitedReason::ReprC => (
1755-
"`repr(C)` types",
1756-
"is a `#[repr(C)]` type, so it is not guaranteed to be zero-sized on all targets.",
1757-
),
1758-
};
1759-
Diag::new(
1760-
dcx,
1761-
level,
1762-
format!("zero-sized fields in `repr(transparent)` cannot contain {title}"),
1763-
)
1764-
.with_note(format!(
1765-
"this field contains `{field_ty}`, which {note}",
1766-
field_ty = unsuited.ty,
1767-
))
1768-
}
1769-
}
1770-
17711738
if !adt.repr().transparent() {
17721739
return;
17731740
}
@@ -1787,108 +1754,137 @@ pub(super) fn check_transparent<'tcx>(tcx: TyCtxt<'tcx>, adt: ty::AdtDef<'tcx>)
17871754
// Don't bother checking the fields.
17881755
return;
17891756
}
1757+
let variant = adt.variant(VariantIdx::ZERO);
17901758

1791-
let typing_env = ty::TypingEnv::non_body_analysis(tcx, adt.did());
1792-
// For each field, figure out if it has "trivial" layout (i.e., is a 1-ZST).
1793-
struct FieldInfo<'tcx> {
1794-
span: Span,
1795-
trivial: bool,
1796-
ty: Ty<'tcx>,
1797-
}
1798-
1799-
let field_infos = adt.all_fields().map(|field| {
1800-
let ty = field.ty(tcx, GenericArgs::identity_for_item(tcx, field.did)).skip_norm_wip();
1801-
let layout = tcx.layout_of(typing_env.as_query_input(ty));
1802-
// We are currently checking the type this field came from, so it must be local
1803-
let span = tcx.hir_span_if_local(field.did).unwrap();
1804-
let trivial = layout.is_ok_and(|layout| layout.is_1zst());
1805-
FieldInfo { span, trivial, ty }
1806-
});
1807-
1808-
let non_trivial_fields = field_infos
1809-
.clone()
1810-
.filter_map(|field| if !field.trivial { Some(field.span) } else { None });
1811-
let non_trivial_count = non_trivial_fields.clone().count();
1812-
if non_trivial_count >= 2 {
1813-
bad_non_zero_sized_fields(
1814-
tcx,
1815-
adt,
1816-
non_trivial_count,
1817-
non_trivial_fields,
1818-
tcx.def_span(adt.did()),
1819-
);
1759+
if variant.fields.len() <= 1 {
1760+
// No need to check when there's at most one field.
18201761
return;
18211762
}
18221763

1823-
// Even some 1-ZST fields are not allowed though, if they have `non_exhaustive` or private
1824-
// fields or `repr(C)`. We call those fields "unsuited".
1825-
struct UnsuitedInfo<'tcx> {
1826-
/// The source of the problem, a type that is found somewhere within the field type.
1827-
ty: Ty<'tcx>,
1828-
reason: UnsuitedReason,
1829-
}
1830-
enum UnsuitedReason {
1831-
NonExhaustive,
1832-
PrivateField,
1833-
ReprC,
1764+
let typing_env = ty::TypingEnv::non_body_analysis(tcx, adt.did());
1765+
1766+
/// We call a field "trivial" for `repr(transparent)` purposes if it can be ignored.
1767+
/// IOW, `repr(transparent)` is allowed if there is at most one non-trivial field.
1768+
/// This enum captures all the reasons why a field might not be "trivial".
1769+
enum NonTrivialReason<'tcx> {
1770+
UnknownLayout,
1771+
NonZeroSized,
1772+
NonTrivialAlignment,
1773+
PrivateField { inside: Ty<'tcx> },
1774+
NonExhaustive { ty: Ty<'tcx> },
1775+
ReprC { ty: Ty<'tcx> },
1776+
}
1777+
struct NonTrivialFieldInfo<'tcx> {
1778+
span: Span,
1779+
reason: NonTrivialReason<'tcx>,
18341780
}
18351781

1836-
fn check_unsuited<'tcx>(
1782+
/// Check if this type is "trivial" for `repr(transparent)`. If not, return the reason why
1783+
/// and the problematic type.
1784+
fn is_trivial<'tcx>(
18371785
tcx: TyCtxt<'tcx>,
18381786
typing_env: ty::TypingEnv<'tcx>,
18391787
ty: Ty<'tcx>,
1840-
) -> ControlFlow<UnsuitedInfo<'tcx>> {
1788+
) -> ControlFlow<NonTrivialReason<'tcx>> {
18411789
// We can encounter projections during traversal, so ensure the type is normalized.
18421790
let ty =
18431791
tcx.try_normalize_erasing_regions(typing_env, Unnormalized::new_wip(ty)).unwrap_or(ty);
18441792
match ty.kind() {
1845-
ty::Tuple(list) => list.iter().try_for_each(|t| check_unsuited(tcx, typing_env, t)),
1846-
ty::Array(ty, _) => check_unsuited(tcx, typing_env, *ty),
1793+
ty::Tuple(list) => list.iter().try_for_each(|t| is_trivial(tcx, typing_env, t)),
1794+
ty::Array(ty, _) => is_trivial(tcx, typing_env, *ty),
18471795
ty::Adt(def, args) => {
18481796
if !def.did().is_local() && !find_attr!(tcx, def.did(), RustcPubTransparent(_)) {
18491797
let non_exhaustive = def.is_variant_list_non_exhaustive()
18501798
|| def.variants().iter().any(ty::VariantDef::is_field_list_non_exhaustive);
1799+
if non_exhaustive {
1800+
return ControlFlow::Break(NonTrivialReason::NonExhaustive { ty });
1801+
}
18511802
let has_priv = def.all_fields().any(|f| !f.vis.is_public());
1852-
if non_exhaustive || has_priv {
1853-
return ControlFlow::Break(UnsuitedInfo {
1854-
ty,
1855-
reason: if non_exhaustive {
1856-
UnsuitedReason::NonExhaustive
1857-
} else {
1858-
UnsuitedReason::PrivateField
1859-
},
1860-
});
1803+
if has_priv {
1804+
return ControlFlow::Break(NonTrivialReason::PrivateField { inside: ty });
18611805
}
18621806
}
18631807
if def.repr().c() {
1864-
return ControlFlow::Break(UnsuitedInfo { ty, reason: UnsuitedReason::ReprC });
1808+
return ControlFlow::Break(NonTrivialReason::ReprC { ty });
18651809
}
18661810
def.all_fields()
18671811
.map(|field| field.ty(tcx, args).skip_norm_wip())
1868-
.try_for_each(|t| check_unsuited(tcx, typing_env, t))
1812+
.try_for_each(|t| is_trivial(tcx, typing_env, t))
18691813
}
18701814
_ => ControlFlow::Continue(()),
18711815
}
18721816
}
18731817

1874-
let mut prev_unsuited_1zst = false;
1875-
for field in field_infos {
1876-
if field.trivial
1877-
&& let Some(unsuited) = check_unsuited(tcx, typing_env, field.ty).break_value()
1878-
{
1879-
// If there are any non-trivial fields, then there can be no non-exhaustive 1-zsts.
1880-
// Otherwise, it's only an issue if there's >1 non-exhaustive 1-zst.
1881-
if non_trivial_count > 0 || prev_unsuited_1zst {
1882-
tcx.emit_node_span_lint(
1883-
REPR_TRANSPARENT_NON_ZST_FIELDS,
1884-
tcx.local_def_id_to_hir_id(adt.did().expect_local()),
1885-
field.span,
1886-
ZeroSizedFieldReprTransparentIncompatibility { unsuited },
1887-
);
1888-
} else {
1889-
prev_unsuited_1zst = true;
1818+
let non_trivial_fields = variant
1819+
.fields
1820+
.iter()
1821+
.filter_map(|field| {
1822+
let ty = field.ty(tcx, GenericArgs::identity_for_item(tcx, field.did)).skip_norm_wip();
1823+
let layout = tcx.layout_of(typing_env.as_query_input(ty));
1824+
// We are currently checking the type this field came from, so it must be local
1825+
let span = tcx.hir_span_if_local(field.did).unwrap();
1826+
// Rule out non-1ZST
1827+
if !layout.is_ok_and(|layout| layout.is_1zst()) {
1828+
let reason = match layout {
1829+
Err(_) => NonTrivialReason::UnknownLayout,
1830+
Ok(layout) => {
1831+
if !(layout.is_sized() && layout.size.bytes() == 0) {
1832+
NonTrivialReason::NonZeroSized
1833+
} else {
1834+
NonTrivialReason::NonTrivialAlignment
1835+
}
1836+
}
1837+
};
1838+
return Some(NonTrivialFieldInfo { span, reason });
1839+
}
1840+
// Recursively check for other things that have to be ruled out.
1841+
if let Some(reason) = is_trivial(tcx, typing_env, ty).break_value() {
1842+
return Some(NonTrivialFieldInfo { span, reason });
18901843
}
1844+
// Otherwise,
1845+
None
1846+
})
1847+
.collect::<Vec<_>>();
1848+
1849+
if non_trivial_fields.len() > 1 {
1850+
let count = non_trivial_fields.len();
1851+
let desc = if adt.is_enum() {
1852+
format_args!("the variant of a transparent {}", adt.descr())
1853+
} else {
1854+
format_args!("transparent {}", adt.descr())
1855+
};
1856+
let ty_span = tcx.def_span(adt.did());
1857+
let mut diag = tcx.dcx().struct_span_err(
1858+
ty_span,
1859+
format!("{desc} needs at most one non-trivial field, but has {count}"),
1860+
);
1861+
diag.code(E0690);
1862+
1863+
// Label for the type.
1864+
diag.span_label(ty_span, format!("needs at most one non-trivial field, but has {count}"));
1865+
// Label for each non-trivial field.
1866+
for field in non_trivial_fields {
1867+
let msg = match field.reason {
1868+
NonTrivialReason::UnknownLayout => {
1869+
format!("this field is generic and hence may have non-zero size")
1870+
}
1871+
NonTrivialReason::NonZeroSized => format!("this field has non-zero size"),
1872+
NonTrivialReason::NonTrivialAlignment => format!("this field requires alignment"),
1873+
NonTrivialReason::PrivateField { inside } => format!(
1874+
"this field contains `{inside}`, which has private fields, so it could become non-zero-sized in the future"
1875+
),
1876+
NonTrivialReason::NonExhaustive { ty } => format!(
1877+
"this field contains `{ty}`, which is marked with `#[non_exhaustive]`, so it could become non-zero-sized in the future"
1878+
),
1879+
NonTrivialReason::ReprC { ty } => format!(
1880+
"this field contains `{ty}`, which is a `#[repr(C)]` type, so it is not guaranteed to be zero-sized on all targets"
1881+
),
1882+
};
1883+
diag.span_label(field.span, msg);
18911884
}
1885+
1886+
diag.emit();
1887+
return;
18921888
}
18931889
}
18941890

compiler/rustc_hir_analysis/src/check/mod.rs

Lines changed: 0 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -590,32 +590,6 @@ fn bad_variant_count<'tcx>(tcx: TyCtxt<'tcx>, adt: ty::AdtDef<'tcx>, sp: Span, d
590590
});
591591
}
592592

593-
/// Emit an error when encountering two or more non-zero-sized fields in a transparent
594-
/// enum.
595-
fn bad_non_zero_sized_fields<'tcx>(
596-
tcx: TyCtxt<'tcx>,
597-
adt: ty::AdtDef<'tcx>,
598-
field_count: usize,
599-
field_spans: impl Iterator<Item = Span>,
600-
sp: Span,
601-
) {
602-
if adt.is_enum() {
603-
tcx.dcx().emit_err(errors::TransparentNonZeroSizedEnum {
604-
span: sp,
605-
spans: field_spans.collect(),
606-
field_count,
607-
desc: adt.descr(),
608-
});
609-
} else {
610-
tcx.dcx().emit_err(errors::TransparentNonZeroSized {
611-
span: sp,
612-
spans: field_spans.collect(),
613-
field_count,
614-
desc: adt.descr(),
615-
});
616-
}
617-
}
618-
619593
// FIXME: Consider moving this method to a more fitting place.
620594
pub fn potentially_plural_count(count: usize, word: &str) -> String {
621595
format!("{} {}{}", count, word, pluralize!(count))

compiler/rustc_hir_analysis/src/errors.rs

Lines changed: 0 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1008,30 +1008,6 @@ pub(crate) struct TransparentEnumVariant {
10081008
pub path: String,
10091009
}
10101010

1011-
#[derive(Diagnostic)]
1012-
#[diag("the variant of a transparent {$desc} needs at most one field with non-trivial size or alignment, but has {$field_count}", code = E0690)]
1013-
pub(crate) struct TransparentNonZeroSizedEnum<'a> {
1014-
#[primary_span]
1015-
#[label("needs at most one field with non-trivial size or alignment, but has {$field_count}")]
1016-
pub span: Span,
1017-
#[label("this field has non-zero size or requires alignment")]
1018-
pub spans: Vec<Span>,
1019-
pub field_count: usize,
1020-
pub desc: &'a str,
1021-
}
1022-
1023-
#[derive(Diagnostic)]
1024-
#[diag("transparent {$desc} needs at most one field with non-trivial size or alignment, but has {$field_count}", code = E0690)]
1025-
pub(crate) struct TransparentNonZeroSized<'a> {
1026-
#[primary_span]
1027-
#[label("needs at most one field with non-trivial size or alignment, but has {$field_count}")]
1028-
pub span: Span,
1029-
#[label("this field has non-zero size or requires alignment")]
1030-
pub spans: Vec<Span>,
1031-
pub field_count: usize,
1032-
pub desc: &'a str,
1033-
}
1034-
10351011
#[derive(Diagnostic)]
10361012
#[diag("extern static is too large for the target architecture")]
10371013
pub(crate) struct TooLargeStatic {

compiler/rustc_lint/src/lib.rs

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -376,10 +376,6 @@ fn register_builtins(store: &mut LintStore) {
376376
store.register_renamed("static_mut_ref", "static_mut_refs");
377377
store.register_renamed("temporary_cstring_as_ptr", "dangling_pointers_from_temporaries");
378378
store.register_renamed("elided_named_lifetimes", "mismatched_lifetime_syntaxes");
379-
store.register_renamed(
380-
"repr_transparent_external_private_fields",
381-
"repr_transparent_non_zst_fields",
382-
);
383379

384380
// These were moved to tool lints, but rustc still sees them when compiling normally, before
385381
// tool lints are registered, so `check_tool_name_for_backwards_compat` doesn't work. Use
@@ -654,6 +650,16 @@ fn register_builtins(store: &mut LintStore) {
654650
"inline_always_mismatching_target_features",
655651
"replaced by a hard error for `#[inline(always)]` with `#[target_feature]`",
656652
);
653+
store.register_removed(
654+
"repr_transparent_external_private_fields",
655+
"converted into hard error, \
656+
see <https://github.com/rust-lang/rust/issues/78586> for more information",
657+
);
658+
store.register_removed(
659+
"repr_transparent_non_zst_fields",
660+
"converted into hard error, \
661+
see <https://github.com/rust-lang/rust/issues/78586> for more information",
662+
);
657663
}
658664

659665
fn register_internals(store: &mut LintStore) {

0 commit comments

Comments
 (0)