about summary refs log tree commit diff
path: root/src
diff options
context:
space:
mode:
authorbors <bors@rust-lang.org>2025-07-16 15:10:02 +0000
committerbors <bors@rust-lang.org>2025-07-16 15:10:02 +0000
commit5795086bdfe7ed988aa53a110bd0692c33d8755b (patch)
tree6bc49365c6fab679afe2ab7fac13b5e86db1c5e9 /src
parent1c6de215099bbe33668de762f9591187f6c25eef (diff)
parent96a9df4cf9c51c656af501a9c82deb700c16e45a (diff)
downloadrust-5795086bdfe7ed988aa53a110bd0692c33d8755b.tar.gz
rust-5795086bdfe7ed988aa53a110bd0692c33d8755b.zip
Auto merge of #144028 - samueltardieu:rollup-x6f9h8n, r=samueltardieu
Rollup of 12 pull requests

Successful merges:

 - rust-lang/rust#142936 (rustdoc-json: Structured attributes)
 - rust-lang/rust#143355 (wrapping shift: remove first bitmask and table)
 - rust-lang/rust#143448 (remote-test-client: Exit code `128 + <signal-number>` instead of `3`)
 - rust-lang/rust#143692 (miri: fix out-of-bounds error for ptrs with negative offsets)
 - rust-lang/rust#143738 (Move several float tests to floats/mod.rs)
 - rust-lang/rust#143920 (Make more of codegen_llvm safe)
 - rust-lang/rust#143921 (Constify `Index` traits)
 - rust-lang/rust#143939 (Add 0323pin as maintainer of NetBSD targets, fix link to pkgsrc-wip and explain.)
 - rust-lang/rust#143948 (Update mdbook to 0.4.52)
 - rust-lang/rust#143957 (tidy: check for invalid file names)
 - rust-lang/rust#143968 (Add tracing to `InterpCx::fn_abi_of_instance/fn_abi_of_fn_ptr`)
 - rust-lang/rust#143990 (Add LocalKey<Cell>::update)

r? `@ghost`
`@rustbot` modify labels: rollup
Diffstat (limited to 'src')
-rw-r--r--src/doc/rustc/src/platform-support/netbsd.md3
-rw-r--r--src/librustdoc/clean/types.rs80
-rw-r--r--src/librustdoc/html/render/mod.rs6
-rw-r--r--src/librustdoc/html/render/print_item.rs3
-rw-r--r--src/librustdoc/json/conversions.rs99
-rw-r--r--src/rustdoc-json-types/lib.rs89
-rw-r--r--src/tools/miri/src/helpers.rs2
-rw-r--r--src/tools/miri/tests/fail/dangling_pointers/out_of_bounds_read_neg_offset.rs6
-rw-r--r--src/tools/miri/tests/fail/dangling_pointers/out_of_bounds_read_neg_offset.stderr21
-rw-r--r--src/tools/miri/tests/fail/intrinsics/ptr_offset_out_of_bounds_neg2.rs6
-rw-r--r--src/tools/miri/tests/fail/intrinsics/ptr_offset_out_of_bounds_neg2.stderr20
-rw-r--r--src/tools/remote-test-client/src/main.rs4
-rw-r--r--src/tools/rustbook/Cargo.lock4
-rw-r--r--src/tools/rustbook/Cargo.toml2
-rw-r--r--src/tools/tidy/src/filenames.rs40
-rw-r--r--src/tools/tidy/src/lib.rs1
-rw-r--r--src/tools/tidy/src/main.rs2
17 files changed, 317 insertions, 71 deletions
diff --git a/src/doc/rustc/src/platform-support/netbsd.md b/src/doc/rustc/src/platform-support/netbsd.md
index e80ff85edad..b173bf82b44 100644
--- a/src/doc/rustc/src/platform-support/netbsd.md
+++ b/src/doc/rustc/src/platform-support/netbsd.md
@@ -32,10 +32,11 @@ are built for NetBSD 8.x but also work on newer OS versions).
 ## Target Maintainers
 
 [@he32](https://github.com/he32)
+[@0323pin](https://github.com/0323pin)
 
 Further contacts:
 
-- [NetBSD/pkgsrc-wip's rust](https://github.com/NetBSD/pkgsrc-wip/blob/master/rust185/Makefile) maintainer (see MAINTAINER variable). This package is part of "pkgsrc work-in-progress" and is used for deployment and testing of new versions of rust
+- [NetBSD/pkgsrc-wip's rust](https://github.com/NetBSD/pkgsrc-wip/blob/master/rust188/Makefile) maintainer (see MAINTAINER variable). This package is part of "pkgsrc work-in-progress" and is used for deployment and testing of new versions of rust.  Note that we have the convention of having multiple rust versions active in pkgsrc-wip at any one time, so the version number is part of the directory name, and from time to time old versions are culled so this is not a fully "stable" link.
 - [NetBSD's pkgsrc lang/rust](https://github.com/NetBSD/pkgsrc/tree/trunk/lang/rust) for the "proper" package in pkgsrc.
 - [NetBSD's pkgsrc lang/rust-bin](https://github.com/NetBSD/pkgsrc/tree/trunk/lang/rust-bin) which re-uses the bootstrap kit as a binary distribution and therefore avoids the rather protracted native build time of rust itself
 
diff --git a/src/librustdoc/clean/types.rs b/src/librustdoc/clean/types.rs
index 3ecd41db2dd..20babc6168b 100644
--- a/src/librustdoc/clean/types.rs
+++ b/src/librustdoc/clean/types.rs
@@ -759,79 +759,48 @@ impl Item {
         Some(tcx.visibility(def_id))
     }
 
-    fn attributes_without_repr(&self, tcx: TyCtxt<'_>, is_json: bool) -> Vec<String> {
-        const ALLOWED_ATTRIBUTES: &[Symbol] =
-            &[sym::export_name, sym::link_section, sym::no_mangle, sym::non_exhaustive];
+    /// Get a list of attributes excluding `#[repr]` to display.
+    ///
+    /// Only used by the HTML output-format.
+    fn attributes_without_repr(&self) -> Vec<String> {
         self.attrs
             .other_attrs
             .iter()
-            .filter_map(|attr| {
-                if let hir::Attribute::Parsed(AttributeKind::LinkSection { name, .. }) = attr {
+            .filter_map(|attr| match attr {
+                hir::Attribute::Parsed(AttributeKind::LinkSection { name, .. }) => {
                     Some(format!("#[link_section = \"{name}\"]"))
                 }
-                // NoMangle is special cased, as it appears in HTML output, and we want to show it in source form, not HIR printing.
-                // It is also used by cargo-semver-checks.
-                else if let hir::Attribute::Parsed(AttributeKind::NoMangle(..)) = attr {
+                hir::Attribute::Parsed(AttributeKind::NoMangle(..)) => {
                     Some("#[no_mangle]".to_string())
-                } else if let hir::Attribute::Parsed(AttributeKind::ExportName { name, .. }) = attr
-                {
+                }
+                hir::Attribute::Parsed(AttributeKind::ExportName { name, .. }) => {
                     Some(format!("#[export_name = \"{name}\"]"))
-                } else if let hir::Attribute::Parsed(AttributeKind::NonExhaustive(..)) = attr {
+                }
+                hir::Attribute::Parsed(AttributeKind::NonExhaustive(..)) => {
                     Some("#[non_exhaustive]".to_string())
-                } else if is_json {
-                    match attr {
-                        // rustdoc-json stores this in `Item::deprecation`, so we
-                        // don't want it it `Item::attrs`.
-                        hir::Attribute::Parsed(AttributeKind::Deprecation { .. }) => None,
-                        // We have separate pretty-printing logic for `#[repr(..)]` attributes.
-                        hir::Attribute::Parsed(AttributeKind::Repr { .. }) => None,
-                        // target_feature is special-cased because cargo-semver-checks uses it
-                        hir::Attribute::Parsed(AttributeKind::TargetFeature(features, _)) => {
-                            let mut output = String::new();
-                            for (i, (feature, _)) in features.iter().enumerate() {
-                                if i != 0 {
-                                    output.push_str(", ");
-                                }
-                                output.push_str(&format!("enable=\"{}\"", feature.as_str()));
-                            }
-                            Some(format!("#[target_feature({output})]"))
-                        }
-                        hir::Attribute::Parsed(AttributeKind::AutomaticallyDerived(..)) => {
-                            Some("#[automatically_derived]".to_string())
-                        }
-                        _ => Some({
-                            let mut s = rustc_hir_pretty::attribute_to_string(&tcx, attr);
-                            assert_eq!(s.pop(), Some('\n'));
-                            s
-                        }),
-                    }
-                } else {
-                    if !attr.has_any_name(ALLOWED_ATTRIBUTES) {
-                        return None;
-                    }
-                    Some(
-                        rustc_hir_pretty::attribute_to_string(&tcx, attr)
-                            .replace("\\\n", "")
-                            .replace('\n', "")
-                            .replace("  ", " "),
-                    )
                 }
+                _ => None,
             })
             .collect()
     }
 
-    pub(crate) fn attributes(&self, tcx: TyCtxt<'_>, cache: &Cache, is_json: bool) -> Vec<String> {
-        let mut attrs = self.attributes_without_repr(tcx, is_json);
+    /// Get a list of attributes to display on this item.
+    ///
+    /// Only used by the HTML output-format.
+    pub(crate) fn attributes(&self, tcx: TyCtxt<'_>, cache: &Cache) -> Vec<String> {
+        let mut attrs = self.attributes_without_repr();
 
-        if let Some(repr_attr) = self.repr(tcx, cache, is_json) {
+        if let Some(repr_attr) = self.repr(tcx, cache) {
             attrs.push(repr_attr);
         }
         attrs
     }
 
     /// Returns a stringified `#[repr(...)]` attribute.
-    pub(crate) fn repr(&self, tcx: TyCtxt<'_>, cache: &Cache, is_json: bool) -> Option<String> {
-        repr_attributes(tcx, cache, self.def_id()?, self.type_(), is_json)
+    ///
+    /// Only used by the HTML output-format.
+    pub(crate) fn repr(&self, tcx: TyCtxt<'_>, cache: &Cache) -> Option<String> {
+        repr_attributes(tcx, cache, self.def_id()?, self.type_())
     }
 
     pub fn is_doc_hidden(&self) -> bool {
@@ -843,12 +812,14 @@ impl Item {
     }
 }
 
+/// Return a string representing the `#[repr]` attribute if present.
+///
+/// Only used by the HTML output-format.
 pub(crate) fn repr_attributes(
     tcx: TyCtxt<'_>,
     cache: &Cache,
     def_id: DefId,
     item_type: ItemType,
-    is_json: bool,
 ) -> Option<String> {
     use rustc_abi::IntegerType;
 
@@ -865,7 +836,6 @@ pub(crate) fn repr_attributes(
         // Render `repr(transparent)` iff the non-1-ZST field is public or at least one
         // field is public in case all fields are 1-ZST fields.
         let render_transparent = cache.document_private
-            || is_json
             || adt
                 .all_fields()
                 .find(|field| {
diff --git a/src/librustdoc/html/render/mod.rs b/src/librustdoc/html/render/mod.rs
index 70f3f54e4c0..06de4944d97 100644
--- a/src/librustdoc/html/render/mod.rs
+++ b/src/librustdoc/html/render/mod.rs
@@ -1191,7 +1191,7 @@ fn render_assoc_item(
 // a whitespace prefix and newline.
 fn render_attributes_in_pre(it: &clean::Item, prefix: &str, cx: &Context<'_>) -> impl fmt::Display {
     fmt::from_fn(move |f| {
-        for a in it.attributes(cx.tcx(), cx.cache(), false) {
+        for a in it.attributes(cx.tcx(), cx.cache()) {
             writeln!(f, "{prefix}{a}")?;
         }
         Ok(())
@@ -1207,7 +1207,7 @@ fn render_code_attribute(code_attr: CodeAttribute, w: &mut impl fmt::Write) {
 // When an attribute is rendered inside a <code> tag, it is formatted using
 // a div to produce a newline after it.
 fn render_attributes_in_code(w: &mut impl fmt::Write, it: &clean::Item, cx: &Context<'_>) {
-    for attr in it.attributes(cx.tcx(), cx.cache(), false) {
+    for attr in it.attributes(cx.tcx(), cx.cache()) {
         render_code_attribute(CodeAttribute(attr), w);
     }
 }
@@ -1219,7 +1219,7 @@ fn render_repr_attributes_in_code(
     def_id: DefId,
     item_type: ItemType,
 ) {
-    if let Some(repr) = clean::repr_attributes(cx.tcx(), cx.cache(), def_id, item_type, false) {
+    if let Some(repr) = clean::repr_attributes(cx.tcx(), cx.cache(), def_id, item_type) {
         render_code_attribute(CodeAttribute(repr), w);
     }
 }
diff --git a/src/librustdoc/html/render/print_item.rs b/src/librustdoc/html/render/print_item.rs
index e33bdc0db32..667d39e9bc2 100644
--- a/src/librustdoc/html/render/print_item.rs
+++ b/src/librustdoc/html/render/print_item.rs
@@ -1487,12 +1487,11 @@ impl<'a, 'cx: 'a> ItemUnion<'a, 'cx> {
                     self.cx.cache(),
                     self.def_id,
                     ItemType::Union,
-                    false,
                 ) {
                     writeln!(f, "{repr}")?;
                 };
             } else {
-                for a in self.it.attributes(self.cx.tcx(), self.cx.cache(), false) {
+                for a in self.it.attributes(self.cx.tcx(), self.cx.cache()) {
                     writeln!(f, "{a}")?;
                 }
             }
diff --git a/src/librustdoc/json/conversions.rs b/src/librustdoc/json/conversions.rs
index e7163bead92..0a84d8caa30 100644
--- a/src/librustdoc/json/conversions.rs
+++ b/src/librustdoc/json/conversions.rs
@@ -5,10 +5,12 @@
 use rustc_abi::ExternAbi;
 use rustc_ast::ast;
 use rustc_attr_data_structures::{self as attrs, DeprecatedSince};
+use rustc_hir as hir;
 use rustc_hir::def::CtorKind;
 use rustc_hir::def_id::DefId;
 use rustc_hir::{HeaderSafety, Safety};
 use rustc_metadata::rendered_const;
+use rustc_middle::ty::TyCtxt;
 use rustc_middle::{bug, ty};
 use rustc_span::{Pos, kw, sym};
 use rustdoc_json_types::*;
@@ -39,7 +41,12 @@ impl JsonRenderer<'_> {
             })
             .collect();
         let docs = item.opt_doc_value();
-        let attrs = item.attributes(self.tcx, &self.cache, true);
+        let attrs = item
+            .attrs
+            .other_attrs
+            .iter()
+            .filter_map(|a| maybe_from_hir_attr(a, item.item_id, self.tcx))
+            .collect();
         let span = item.span(self.tcx);
         let visibility = item.visibility(self.tcx);
         let clean::ItemInner { name, item_id, .. } = *item.inner;
@@ -886,3 +893,93 @@ impl FromClean<ItemType> for ItemKind {
         }
     }
 }
+
+/// Maybe convert a attribute from hir to json.
+///
+/// Returns `None` if the attribute shouldn't be in the output.
+fn maybe_from_hir_attr(
+    attr: &hir::Attribute,
+    item_id: ItemId,
+    tcx: TyCtxt<'_>,
+) -> Option<Attribute> {
+    use attrs::AttributeKind as AK;
+
+    let kind = match attr {
+        hir::Attribute::Parsed(kind) => kind,
+
+        hir::Attribute::Unparsed(_) => {
+            // FIXME: We should handle `#[doc(hidden)]`.
+            return Some(other_attr(tcx, attr));
+        }
+    };
+
+    Some(match kind {
+        AK::Deprecation { .. } => return None, // Handled separately into Item::deprecation.
+        AK::DocComment { .. } => unreachable!("doc comments stripped out earlier"),
+
+        AK::MustUse { reason, span: _ } => {
+            Attribute::MustUse { reason: reason.map(|s| s.to_string()) }
+        }
+        AK::Repr { .. } => repr_attr(
+            tcx,
+            item_id.as_def_id().expect("all items that could have #[repr] have a DefId"),
+        ),
+        AK::ExportName { name, span: _ } => Attribute::ExportName(name.to_string()),
+        AK::LinkSection { name, span: _ } => Attribute::LinkSection(name.to_string()),
+        AK::TargetFeature(features, _span) => Attribute::TargetFeature {
+            enable: features.iter().map(|(feat, _span)| feat.to_string()).collect(),
+        },
+
+        AK::NoMangle(_) => Attribute::NoMangle,
+        AK::NonExhaustive(_) => Attribute::NonExhaustive,
+        AK::AutomaticallyDerived(_) => Attribute::AutomaticallyDerived,
+
+        _ => other_attr(tcx, attr),
+    })
+}
+
+fn other_attr(tcx: TyCtxt<'_>, attr: &hir::Attribute) -> Attribute {
+    let mut s = rustc_hir_pretty::attribute_to_string(&tcx, attr);
+    assert_eq!(s.pop(), Some('\n'));
+    Attribute::Other(s)
+}
+
+fn repr_attr(tcx: TyCtxt<'_>, def_id: DefId) -> Attribute {
+    let repr = tcx.adt_def(def_id).repr();
+
+    let kind = if repr.c() {
+        ReprKind::C
+    } else if repr.transparent() {
+        ReprKind::Transparent
+    } else if repr.simd() {
+        ReprKind::Simd
+    } else {
+        ReprKind::Rust
+    };
+
+    let align = repr.align.map(|a| a.bytes());
+    let packed = repr.pack.map(|p| p.bytes());
+    let int = repr.int.map(format_integer_type);
+
+    Attribute::Repr(AttributeRepr { kind, align, packed, int })
+}
+
+fn format_integer_type(it: rustc_abi::IntegerType) -> String {
+    use rustc_abi::Integer::*;
+    use rustc_abi::IntegerType::*;
+    match it {
+        Pointer(true) => "isize",
+        Pointer(false) => "usize",
+        Fixed(I8, true) => "i8",
+        Fixed(I8, false) => "u8",
+        Fixed(I16, true) => "i16",
+        Fixed(I16, false) => "u16",
+        Fixed(I32, true) => "i32",
+        Fixed(I32, false) => "u32",
+        Fixed(I64, true) => "i64",
+        Fixed(I64, false) => "u64",
+        Fixed(I128, true) => "i128",
+        Fixed(I128, false) => "u128",
+    }
+    .to_owned()
+}
diff --git a/src/rustdoc-json-types/lib.rs b/src/rustdoc-json-types/lib.rs
index 0e72ddd9db1..6235b0e8576 100644
--- a/src/rustdoc-json-types/lib.rs
+++ b/src/rustdoc-json-types/lib.rs
@@ -37,8 +37,8 @@ pub type FxHashMap<K, V> = HashMap<K, V>; // re-export for use in src/librustdoc
 // will instead cause conflicts. See #94591 for more. (This paragraph and the "Latest feature" line
 // are deliberately not in a doc comment, because they need not be in public docs.)
 //
-// Latest feature: Pretty printing of no_mangle attributes changed
-pub const FORMAT_VERSION: u32 = 53;
+// Latest feature: Structured Attributes
+pub const FORMAT_VERSION: u32 = 54;
 
 /// The root of the emitted JSON blob.
 ///
@@ -195,13 +195,94 @@ pub struct Item {
     /// - `#[repr(C)]` and other reprs also appear as themselves,
     ///   though potentially with a different order: e.g. `repr(i8, C)` may become `repr(C, i8)`.
     ///   Multiple repr attributes on the same item may be combined into an equivalent single attr.
-    pub attrs: Vec<String>,
+    pub attrs: Vec<Attribute>,
     /// Information about the item’s deprecation, if present.
     pub deprecation: Option<Deprecation>,
     /// The type-specific fields describing this item.
     pub inner: ItemEnum,
 }
 
+#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
+#[serde(rename_all = "snake_case")]
+/// An attribute, e.g. `#[repr(C)]`
+///
+/// This doesn't include:
+/// - `#[doc = "Doc Comment"]` or `/// Doc comment`. These are in [`Item::docs`] instead.
+/// - `#[deprecated]`. These are in [`Item::deprecation`] instead.
+pub enum Attribute {
+    /// `#[non_exhaustive]`
+    NonExhaustive,
+
+    /// `#[must_use]`
+    MustUse { reason: Option<String> },
+
+    /// `#[export_name = "name"]`
+    ExportName(String),
+
+    /// `#[link_section = "name"]`
+    LinkSection(String),
+
+    /// `#[automatically_derived]`
+    AutomaticallyDerived,
+
+    /// `#[repr]`
+    Repr(AttributeRepr),
+
+    /// `#[no_mangle]`
+    NoMangle,
+
+    /// #[target_feature(enable = "feature1", enable = "feature2")]
+    TargetFeature { enable: Vec<String> },
+
+    /// Something else.
+    ///
+    /// Things here are explicitly *not* covered by the [`FORMAT_VERSION`]
+    /// constant, and may change without bumping the format version.
+    ///
+    /// As an implementation detail, this is currently either:
+    /// 1. A HIR debug printing, like `"#[attr = Optimize(Speed)]"`
+    /// 2. The attribute as it appears in source form, like
+    ///    `"#[optimize(speed)]"`.
+    Other(String),
+}
+
+#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
+/// The contents of a `#[repr(...)]` attribute.
+///
+/// Used in [`Attribute::Repr`].
+pub struct AttributeRepr {
+    /// The representation, e.g. `#[repr(C)]`, `#[repr(transparent)]`
+    pub kind: ReprKind,
+
+    /// Alignment in bytes, if explicitly specified by `#[repr(align(...)]`.
+    pub align: Option<u64>,
+    /// Alignment in bytes, if explicitly specified by `#[repr(packed(...)]]`.
+    pub packed: Option<u64>,
+
+    /// The integer type for an enum descriminant, if explicitly specified.
+    ///
+    /// e.g. `"i32"`, for `#[repr(C, i32)]`
+    pub int: Option<String>,
+}
+
+#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
+#[serde(rename_all = "snake_case")]
+/// The kind of `#[repr]`.
+///
+/// See [AttributeRepr::kind]`.
+pub enum ReprKind {
+    /// `#[repr(Rust)]`
+    ///
+    /// Also the default.
+    Rust,
+    /// `#[repr(C)]`
+    C,
+    /// `#[repr(transparent)]
+    Transparent,
+    /// `#[repr(simd)]`
+    Simd,
+}
+
 /// A range of source code.
 #[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
 pub struct Span {
@@ -1343,7 +1424,7 @@ pub struct Static {
 
     /// Is the static `unsafe`?
     ///
-    /// This is only true if it's in an `extern` block, and not explicity marked
+    /// This is only true if it's in an `extern` block, and not explicitly marked
     /// as `safe`.
     ///
     /// ```rust
diff --git a/src/tools/miri/src/helpers.rs b/src/tools/miri/src/helpers.rs
index c150dc16b07..f2283f0902b 100644
--- a/src/tools/miri/src/helpers.rs
+++ b/src/tools/miri/src/helpers.rs
@@ -13,7 +13,7 @@ use rustc_index::IndexVec;
 use rustc_middle::middle::codegen_fn_attrs::CodegenFnAttrFlags;
 use rustc_middle::middle::dependency_format::Linkage;
 use rustc_middle::middle::exported_symbols::ExportedSymbol;
-use rustc_middle::ty::layout::{FnAbiOf, LayoutOf, MaybeResult, TyAndLayout};
+use rustc_middle::ty::layout::{LayoutOf, MaybeResult, TyAndLayout};
 use rustc_middle::ty::{self, Binder, FloatTy, FnSig, IntTy, Ty, TyCtxt, UintTy};
 use rustc_session::config::CrateType;
 use rustc_span::{Span, Symbol};
diff --git a/src/tools/miri/tests/fail/dangling_pointers/out_of_bounds_read_neg_offset.rs b/src/tools/miri/tests/fail/dangling_pointers/out_of_bounds_read_neg_offset.rs
new file mode 100644
index 00000000000..107a3db91d8
--- /dev/null
+++ b/src/tools/miri/tests/fail/dangling_pointers/out_of_bounds_read_neg_offset.rs
@@ -0,0 +1,6 @@
+fn main() {
+    let v: Vec<u16> = vec![1, 2];
+    // This read is also misaligned. We make sure that the OOB message has priority.
+    let x = unsafe { *v.as_ptr().wrapping_byte_sub(5) }; //~ ERROR: before the beginning of the allocation
+    panic!("this should never print: {}", x);
+}
diff --git a/src/tools/miri/tests/fail/dangling_pointers/out_of_bounds_read_neg_offset.stderr b/src/tools/miri/tests/fail/dangling_pointers/out_of_bounds_read_neg_offset.stderr
new file mode 100644
index 00000000000..5c37caa1ebf
--- /dev/null
+++ b/src/tools/miri/tests/fail/dangling_pointers/out_of_bounds_read_neg_offset.stderr
@@ -0,0 +1,21 @@
+error: Undefined Behavior: memory access failed: attempting to access 2 bytes, but got ALLOC-0x5 which points to before the beginning of the allocation
+  --> tests/fail/dangling_pointers/out_of_bounds_read_neg_offset.rs:LL:CC
+   |
+LL |     let x = unsafe { *v.as_ptr().wrapping_byte_sub(5) };
+   |                      ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Undefined Behavior occurred here
+   |
+   = help: this indicates a bug in the program: it performed an invalid operation, and caused Undefined Behavior
+   = help: see https://doc.rust-lang.org/nightly/reference/behavior-considered-undefined.html for further information
+help: ALLOC was allocated here:
+  --> tests/fail/dangling_pointers/out_of_bounds_read_neg_offset.rs:LL:CC
+   |
+LL |     let v: Vec<u16> = vec![1, 2];
+   |                       ^^^^^^^^^^
+   = note: BACKTRACE (of the first span):
+   = note: inside `main` at tests/fail/dangling_pointers/out_of_bounds_read_neg_offset.rs:LL:CC
+   = note: this error originates in the macro `vec` (in Nightly builds, run with -Z macro-backtrace for more info)
+
+note: some details are omitted, run with `MIRIFLAGS=-Zmiri-backtrace=full` for a verbose backtrace
+
+error: aborting due to 1 previous error
+
diff --git a/src/tools/miri/tests/fail/intrinsics/ptr_offset_out_of_bounds_neg2.rs b/src/tools/miri/tests/fail/intrinsics/ptr_offset_out_of_bounds_neg2.rs
new file mode 100644
index 00000000000..0085e2af367
--- /dev/null
+++ b/src/tools/miri/tests/fail/intrinsics/ptr_offset_out_of_bounds_neg2.rs
@@ -0,0 +1,6 @@
+fn main() {
+    let v = [0i8; 4];
+    let x = &v as *const i8;
+    let x = unsafe { x.wrapping_offset(-1).offset(-1) }; //~ERROR: before the beginning of the allocation
+    panic!("this should never print: {:?}", x);
+}
diff --git a/src/tools/miri/tests/fail/intrinsics/ptr_offset_out_of_bounds_neg2.stderr b/src/tools/miri/tests/fail/intrinsics/ptr_offset_out_of_bounds_neg2.stderr
new file mode 100644
index 00000000000..495aaf8d40e
--- /dev/null
+++ b/src/tools/miri/tests/fail/intrinsics/ptr_offset_out_of_bounds_neg2.stderr
@@ -0,0 +1,20 @@
+error: Undefined Behavior: in-bounds pointer arithmetic failed: attempting to offset pointer by -1 bytes, but got ALLOC-0x1 which points to before the beginning of the allocation
+  --> tests/fail/intrinsics/ptr_offset_out_of_bounds_neg2.rs:LL:CC
+   |
+LL |     let x = unsafe { x.wrapping_offset(-1).offset(-1) };
+   |                      ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Undefined Behavior occurred here
+   |
+   = help: this indicates a bug in the program: it performed an invalid operation, and caused Undefined Behavior
+   = help: see https://doc.rust-lang.org/nightly/reference/behavior-considered-undefined.html for further information
+help: ALLOC was allocated here:
+  --> tests/fail/intrinsics/ptr_offset_out_of_bounds_neg2.rs:LL:CC
+   |
+LL |     let v = [0i8; 4];
+   |         ^
+   = note: BACKTRACE (of the first span):
+   = note: inside `main` at tests/fail/intrinsics/ptr_offset_out_of_bounds_neg2.rs:LL:CC
+
+note: some details are omitted, run with `MIRIFLAGS=-Zmiri-backtrace=full` for a verbose backtrace
+
+error: aborting due to 1 previous error
+
diff --git a/src/tools/remote-test-client/src/main.rs b/src/tools/remote-test-client/src/main.rs
index bda08423487..b9741431b50 100644
--- a/src/tools/remote-test-client/src/main.rs
+++ b/src/tools/remote-test-client/src/main.rs
@@ -335,7 +335,9 @@ fn run(support_lib_count: usize, exe: String, all_args: Vec<String>) {
         std::process::exit(code);
     } else {
         println!("died due to signal {}", code);
-        std::process::exit(3);
+        // Behave like bash and other tools and exit with 128 + the signal
+        // number. That way we can avoid special case code in other places.
+        std::process::exit(128 + code);
     }
 }
 
diff --git a/src/tools/rustbook/Cargo.lock b/src/tools/rustbook/Cargo.lock
index 050ddf47bae..27798d6aeb0 100644
--- a/src/tools/rustbook/Cargo.lock
+++ b/src/tools/rustbook/Cargo.lock
@@ -885,9 +885,9 @@ dependencies = [
 
 [[package]]
 name = "mdbook"
-version = "0.4.51"
+version = "0.4.52"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a87e65420ab45ca9c1b8cdf698f95b710cc826d373fa550f0f7fad82beac9328"
+checksum = "93c284d2855916af7c5919cf9ad897cfc77d3c2db6f55429c7cfb769182030ec"
 dependencies = [
  "ammonia",
  "anyhow",
diff --git a/src/tools/rustbook/Cargo.toml b/src/tools/rustbook/Cargo.toml
index 69c0cfaf5c9..c7c6e39f157 100644
--- a/src/tools/rustbook/Cargo.toml
+++ b/src/tools/rustbook/Cargo.toml
@@ -15,6 +15,6 @@ mdbook-i18n-helpers = "0.3.3"
 mdbook-spec = { path = "../../doc/reference/mdbook-spec" }
 
 [dependencies.mdbook]
-version = "0.4.51"
+version = "0.4.52"
 default-features = false
 features = ["search"]
diff --git a/src/tools/tidy/src/filenames.rs b/src/tools/tidy/src/filenames.rs
new file mode 100644
index 00000000000..53115f4eaa4
--- /dev/null
+++ b/src/tools/tidy/src/filenames.rs
@@ -0,0 +1,40 @@
+//! Tidy check to ensure that there are no filenames containing forbidden characters
+//! checked into the source tree by accident:
+//! - Non-UTF8 filenames
+//! - Control characters such as CR or TAB
+//! - Filenames containing ":" as they are not supported on Windows
+//!
+//! Only files added to git are checked, as it may be acceptable to have temporary
+//! invalid filenames in the local directory during development.
+
+use std::path::Path;
+use std::process::Command;
+
+pub fn check(root_path: &Path, bad: &mut bool) {
+    let stat_output = Command::new("git")
+        .arg("-C")
+        .arg(root_path)
+        .args(["ls-files", "-z"])
+        .output()
+        .unwrap()
+        .stdout;
+    for filename in stat_output.split(|&b| b == 0) {
+        match str::from_utf8(filename) {
+            Err(_) => tidy_error!(
+                bad,
+                r#"non-UTF8 file names are not supported: "{}""#,
+                String::from_utf8_lossy(filename),
+            ),
+            Ok(name) if name.chars().any(|c| c.is_control()) => tidy_error!(
+                bad,
+                r#"control characters are not supported in file names: "{}""#,
+                String::from_utf8_lossy(filename),
+            ),
+            Ok(name) if name.contains(':') => tidy_error!(
+                bad,
+                r#"":" is not supported in file names because of Windows compatibility: "{name}""#,
+            ),
+            _ => (),
+        }
+    }
+}
diff --git a/src/tools/tidy/src/lib.rs b/src/tools/tidy/src/lib.rs
index 77855392b4d..ade4055b5bd 100644
--- a/src/tools/tidy/src/lib.rs
+++ b/src/tools/tidy/src/lib.rs
@@ -167,6 +167,7 @@ pub mod error_codes;
 pub mod ext_tool_checks;
 pub mod extdeps;
 pub mod features;
+pub mod filenames;
 pub mod fluent_alphabetical;
 pub mod fluent_period;
 mod fluent_used;
diff --git a/src/tools/tidy/src/main.rs b/src/tools/tidy/src/main.rs
index a67f7a511b5..0f1116a632e 100644
--- a/src/tools/tidy/src/main.rs
+++ b/src/tools/tidy/src/main.rs
@@ -155,6 +155,8 @@ fn main() {
 
         check!(triagebot, &root_path);
 
+        check!(filenames, &root_path);
+
         let collected = {
             drain_handles(&mut handles);