about summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--library/std/src/sys/thread_local/mod.rs2
-rw-r--r--library/std/src/sys/thread_local/native/eager.rs4
-rw-r--r--library/std/src/sys/thread_local/native/lazy.rs4
-rw-r--r--library/std/src/sys/thread_local/native/mod.rs12
-rw-r--r--library/std/src/sys/thread_local/no_threads.rs69
-rw-r--r--library/std/src/sys/thread_local/os.rs155
-rw-r--r--library/std/src/thread/local.rs233
-rw-r--r--library/std/src/thread/mod.rs1
-rw-r--r--library/std/tests/thread.rs2
-rw-r--r--src/tools/miri/tests/pass/static_align.rs60
-rw-r--r--src/tools/miri/tests/pass/thread_local-panic.rs8
-rw-r--r--src/tools/miri/tests/pass/thread_local-panic.stderr5
-rw-r--r--tests/ui/feature-gates/feature-gate-static_align-thread_local.rs11
-rw-r--r--tests/ui/macros/macro-local-data-key-priv.stderr2
-rw-r--r--tests/ui/static/static-align.rs84
-rw-r--r--tests/ui/thread-local/long-docs.rs266
-rw-r--r--tests/ui/thread-local/no-unstable.rs17
-rw-r--r--tests/ui/thread-local/no-unstable.stderr57
18 files changed, 917 insertions, 75 deletions
diff --git a/library/std/src/sys/thread_local/mod.rs b/library/std/src/sys/thread_local/mod.rs
index d5c795093cf..f7f051b1add 100644
--- a/library/std/src/sys/thread_local/mod.rs
+++ b/library/std/src/sys/thread_local/mod.rs
@@ -42,7 +42,7 @@ cfg_select! {
     }
     _ => {
         mod os;
-        pub use os::{Storage, thread_local_inner};
+        pub use os::{Storage, thread_local_inner, value_align};
         pub(crate) use os::{LocalPointer, local_pointer};
     }
 }
diff --git a/library/std/src/sys/thread_local/native/eager.rs b/library/std/src/sys/thread_local/native/eager.rs
index fd48c4f7202..23abad645c1 100644
--- a/library/std/src/sys/thread_local/native/eager.rs
+++ b/library/std/src/sys/thread_local/native/eager.rs
@@ -10,9 +10,11 @@ enum State {
 }
 
 #[allow(missing_debug_implementations)]
+#[repr(C)]
 pub struct Storage<T> {
-    state: Cell<State>,
+    // This field must be first, for correctness of `#[rustc_align_static]`
     val: UnsafeCell<T>,
+    state: Cell<State>,
 }
 
 impl<T> Storage<T> {
diff --git a/library/std/src/sys/thread_local/native/lazy.rs b/library/std/src/sys/thread_local/native/lazy.rs
index b556dd9aa25..02939a74fc0 100644
--- a/library/std/src/sys/thread_local/native/lazy.rs
+++ b/library/std/src/sys/thread_local/native/lazy.rs
@@ -27,9 +27,11 @@ enum State<D> {
 }
 
 #[allow(missing_debug_implementations)]
+#[repr(C)]
 pub struct Storage<T, D> {
-    state: Cell<State<D>>,
+    // This field must be first, for correctness of `#[rustc_align_static]`
     value: UnsafeCell<MaybeUninit<T>>,
+    state: Cell<State<D>>,
 }
 
 impl<T, D> Storage<T, D>
diff --git a/library/std/src/sys/thread_local/native/mod.rs b/library/std/src/sys/thread_local/native/mod.rs
index a5dffe3c458..5dc14240804 100644
--- a/library/std/src/sys/thread_local/native/mod.rs
+++ b/library/std/src/sys/thread_local/native/mod.rs
@@ -54,7 +54,7 @@ pub macro thread_local_inner {
     // test in `tests/thread.rs` if these types are renamed.
 
     // Used to generate the `LocalKey` value for const-initialized thread locals.
-    (@key $t:ty, const $init:expr) => {{
+    (@key $t:ty, $(#[$align_attr:meta])*, const $init:expr) => {{
         const __INIT: $t = $init;
 
         unsafe {
@@ -62,6 +62,7 @@ pub macro thread_local_inner {
                 if $crate::mem::needs_drop::<$t>() {
                     |_| {
                         #[thread_local]
+                        $(#[$align_attr])*
                         static VAL: $crate::thread::local_impl::EagerStorage<$t>
                             = $crate::thread::local_impl::EagerStorage::new(__INIT);
                         VAL.get()
@@ -69,6 +70,7 @@ pub macro thread_local_inner {
                 } else {
                     |_| {
                         #[thread_local]
+                        $(#[$align_attr])*
                         static VAL: $t = __INIT;
                         &VAL
                     }
@@ -78,7 +80,7 @@ pub macro thread_local_inner {
     }},
 
     // used to generate the `LocalKey` value for `thread_local!`
-    (@key $t:ty, $init:expr) => {{
+    (@key $t:ty, $(#[$align_attr:meta])*, $init:expr) => {{
         #[inline]
         fn __init() -> $t {
             $init
@@ -89,6 +91,7 @@ pub macro thread_local_inner {
                 if $crate::mem::needs_drop::<$t>() {
                     |init| {
                         #[thread_local]
+                        $(#[$align_attr])*
                         static VAL: $crate::thread::local_impl::LazyStorage<$t, ()>
                             = $crate::thread::local_impl::LazyStorage::new();
                         VAL.get_or_init(init, __init)
@@ -96,6 +99,7 @@ pub macro thread_local_inner {
                 } else {
                     |init| {
                         #[thread_local]
+                        $(#[$align_attr])*
                         static VAL: $crate::thread::local_impl::LazyStorage<$t, !>
                             = $crate::thread::local_impl::LazyStorage::new();
                         VAL.get_or_init(init, __init)
@@ -104,10 +108,6 @@ pub macro thread_local_inner {
             })
         }
     }},
-    ($(#[$attr:meta])* $vis:vis $name:ident, $t:ty, $($init:tt)*) => {
-        $(#[$attr])* $vis const $name: $crate::thread::LocalKey<$t> =
-            $crate::thread::local_impl::thread_local_inner!(@key $t, $($init)*);
-    },
 }
 
 #[rustc_macro_transparency = "semitransparent"]
diff --git a/library/std/src/sys/thread_local/no_threads.rs b/library/std/src/sys/thread_local/no_threads.rs
index 4da01a84acf..409dfb19518 100644
--- a/library/std/src/sys/thread_local/no_threads.rs
+++ b/library/std/src/sys/thread_local/no_threads.rs
@@ -2,6 +2,7 @@
 //! thread locals and we can instead just use plain statics!
 
 use crate::cell::{Cell, UnsafeCell};
+use crate::mem::MaybeUninit;
 use crate::ptr;
 
 #[doc(hidden)]
@@ -11,12 +12,13 @@ use crate::ptr;
 #[rustc_macro_transparency = "semitransparent"]
 pub macro thread_local_inner {
     // used to generate the `LocalKey` value for const-initialized thread locals
-    (@key $t:ty, const $init:expr) => {{
+    (@key $t:ty, $(#[$align_attr:meta])*, const $init:expr) => {{
         const __INIT: $t = $init;
 
         // NOTE: Please update the shadowing test in `tests/thread.rs` if these types are renamed.
         unsafe {
             $crate::thread::LocalKey::new(|_| {
+                $(#[$align_attr])*
                 static VAL: $crate::thread::local_impl::EagerStorage<$t> =
                     $crate::thread::local_impl::EagerStorage { value: __INIT };
                 &VAL.value
@@ -25,27 +27,22 @@ pub macro thread_local_inner {
     }},
 
     // used to generate the `LocalKey` value for `thread_local!`
-    (@key $t:ty, $init:expr) => {{
+    (@key $t:ty, $(#[$align_attr:meta])*, $init:expr) => {{
         #[inline]
         fn __init() -> $t { $init }
 
         unsafe {
-            use $crate::thread::LocalKey;
-            use $crate::thread::local_impl::LazyStorage;
-
-            LocalKey::new(|init| {
-                static VAL: LazyStorage<$t> = LazyStorage::new();
+            $crate::thread::LocalKey::new(|init| {
+                $(#[$align_attr])*
+                static VAL: $crate::thread::local_impl::LazyStorage<$t> = $crate::thread::local_impl::LazyStorage::new();
                 VAL.get(init, __init)
             })
         }
     }},
-    ($(#[$attr:meta])* $vis:vis $name:ident, $t:ty, $($init:tt)*) => {
-        $(#[$attr])* $vis const $name: $crate::thread::LocalKey<$t> =
-            $crate::thread::local_impl::thread_local_inner!(@key $t, $($init)*);
-    },
 }
 
 #[allow(missing_debug_implementations)]
+#[repr(transparent)] // Required for correctness of `#[rustc_align_static]`
 pub struct EagerStorage<T> {
     pub value: T,
 }
@@ -53,14 +50,27 @@ pub struct EagerStorage<T> {
 // SAFETY: the target doesn't have threads.
 unsafe impl<T> Sync for EagerStorage<T> {}
 
+#[derive(Clone, Copy, PartialEq, Eq)]
+enum State {
+    Initial,
+    Alive,
+    Destroying,
+}
+
 #[allow(missing_debug_implementations)]
+#[repr(C)]
 pub struct LazyStorage<T> {
-    value: UnsafeCell<Option<T>>,
+    // This field must be first, for correctness of `#[rustc_align_static]`
+    value: UnsafeCell<MaybeUninit<T>>,
+    state: Cell<State>,
 }
 
 impl<T> LazyStorage<T> {
     pub const fn new() -> LazyStorage<T> {
-        LazyStorage { value: UnsafeCell::new(None) }
+        LazyStorage {
+            value: UnsafeCell::new(MaybeUninit::uninit()),
+            state: Cell::new(State::Initial),
+        }
     }
 
     /// Gets a pointer to the TLS value, potentially initializing it with the
@@ -70,24 +80,39 @@ impl<T> LazyStorage<T> {
     /// has occurred.
     #[inline]
     pub fn get(&'static self, i: Option<&mut Option<T>>, f: impl FnOnce() -> T) -> *const T {
-        let value = unsafe { &*self.value.get() };
-        match value {
-            Some(v) => v,
-            None => self.initialize(i, f),
+        if self.state.get() == State::Alive {
+            self.value.get() as *const T
+        } else {
+            self.initialize(i, f)
         }
     }
 
     #[cold]
     fn initialize(&'static self, i: Option<&mut Option<T>>, f: impl FnOnce() -> T) -> *const T {
         let value = i.and_then(Option::take).unwrap_or_else(f);
-        // Destroy the old value, after updating the TLS variable as the
-        // destructor might reference it.
+
+        // Destroy the old value if it is initialized
         // FIXME(#110897): maybe panic on recursive initialization.
+        if self.state.get() == State::Alive {
+            self.state.set(State::Destroying);
+            // Safety: we check for no initialization during drop below
+            unsafe {
+                ptr::drop_in_place(self.value.get() as *mut T);
+            }
+            self.state.set(State::Initial);
+        }
+
+        // Guard against initialization during drop
+        if self.state.get() == State::Destroying {
+            panic!("Attempted to initialize thread-local while it is being dropped");
+        }
+
         unsafe {
-            self.value.get().replace(Some(value));
+            self.value.get().write(MaybeUninit::new(value));
         }
-        // SAFETY: we just set this to `Some`.
-        unsafe { (*self.value.get()).as_ref().unwrap_unchecked() }
+        self.state.set(State::Alive);
+
+        self.value.get() as *const T
     }
 }
 
diff --git a/library/std/src/sys/thread_local/os.rs b/library/std/src/sys/thread_local/os.rs
index fe6af27db3a..88bb5ae7c65 100644
--- a/library/std/src/sys/thread_local/os.rs
+++ b/library/std/src/sys/thread_local/os.rs
@@ -1,8 +1,12 @@
 use super::key::{Key, LazyKey, get, set};
 use super::{abort_on_dtor_unwind, guard};
+use crate::alloc::{self, Layout};
 use crate::cell::Cell;
 use crate::marker::PhantomData;
-use crate::ptr;
+use crate::mem::ManuallyDrop;
+use crate::ops::Deref;
+use crate::panic::{AssertUnwindSafe, catch_unwind, resume_unwind};
+use crate::ptr::{self, NonNull};
 
 #[doc(hidden)]
 #[allow_internal_unstable(thread_local_internals)]
@@ -10,17 +14,12 @@ use crate::ptr;
 #[unstable(feature = "thread_local_internals", issue = "none")]
 #[rustc_macro_transparency = "semitransparent"]
 pub macro thread_local_inner {
-    // used to generate the `LocalKey` value for const-initialized thread locals
-    (@key $t:ty, const $init:expr) => {
-        $crate::thread::local_impl::thread_local_inner!(@key $t, { const INIT_EXPR: $t = $init; INIT_EXPR })
-    },
-
     // NOTE: we cannot import `Storage` or `LocalKey` with a `use` because that can shadow user
     // provided type or type alias with a matching name. Please update the shadowing test in
     // `tests/thread.rs` if these types are renamed.
 
     // used to generate the `LocalKey` value for `thread_local!`.
-    (@key $t:ty, $init:expr) => {{
+    (@key $t:ty, $($(#[$($align_attr:tt)*])+)?, $init:expr) => {{
         #[inline]
         fn __init() -> $t { $init }
 
@@ -29,37 +28,148 @@ pub macro thread_local_inner {
         // in `tests/thread.rs` if these types are renamed.
         unsafe {
             $crate::thread::LocalKey::new(|init| {
-                static VAL: $crate::thread::local_impl::Storage<$t>
+                static VAL: $crate::thread::local_impl::Storage<$t, {
+                    $({
+                        // Ensure that attributes have valid syntax
+                        // and that the proper feature gate is enabled
+                        $(#[$($align_attr)*])+
+                        #[allow(unused)]
+                        static DUMMY: () = ();
+                    })?
+
+                    #[allow(unused_mut)]
+                    let mut final_align = $crate::thread::local_impl::value_align::<$t>();
+                    $($($crate::thread::local_impl::thread_local_inner!(@align final_align, $($align_attr)*);)+)?
+                    final_align
+                }>
                     = $crate::thread::local_impl::Storage::new();
                 VAL.get(init, __init)
             })
         }
     }},
-    ($(#[$attr:meta])* $vis:vis $name:ident, $t:ty, $($init:tt)*) => {
-        $(#[$attr])* $vis const $name: $crate::thread::LocalKey<$t> =
-            $crate::thread::local_impl::thread_local_inner!(@key $t, $($init)*);
+
+    // process a single `rustc_align_static` attribute
+    (@align $final_align:ident, rustc_align_static($($align:tt)*) $(, $($attr_rest:tt)+)?) => {
+        let new_align: $crate::primitive::usize = $($align)*;
+        if new_align > $final_align {
+            $final_align = new_align;
+        }
+
+        $($crate::thread::local_impl::thread_local_inner!(@align $final_align, $($attr_rest)+);)?
+    },
+
+    // process a single `cfg_attr` attribute
+    // by translating it into a `cfg`ed block and recursing.
+    // https://doc.rust-lang.org/reference/conditional-compilation.html#railroad-ConfigurationPredicate
+
+    (@align $final_align:ident, cfg_attr(true, $($cfg_rhs:tt)*) $(, $($attr_rest:tt)+)?) => {
+        #[cfg(true)]
+        {
+            $crate::thread::local_impl::thread_local_inner!(@align $final_align, $($cfg_rhs)*);
+        }
+
+        $($crate::thread::local_impl::thread_local_inner!(@align $final_align, $($attr_rest)+);)?
+    },
+
+    (@align $final_align:ident, cfg_attr(false, $($cfg_rhs:tt)*) $(, $($attr_rest:tt)+)?) => {
+        #[cfg(false)]
+        {
+            $crate::thread::local_impl::thread_local_inner!(@align $final_align, $($cfg_rhs)*);
+        }
+
+        $($crate::thread::local_impl::thread_local_inner!(@align $final_align, $($attr_rest)+);)?
+    },
+
+    (@align $final_align:ident, cfg_attr($cfg_pred:meta, $($cfg_rhs:tt)*) $(, $($attr_rest:tt)+)?) => {
+        #[cfg($cfg_pred)]
+        {
+            $crate::thread::local_impl::thread_local_inner!(@align $final_align, $($cfg_rhs)*);
+        }
+
+        $($crate::thread::local_impl::thread_local_inner!(@align $final_align, $($attr_rest)+);)?
     },
 }
 
 /// Use a regular global static to store this key; the state provided will then be
 /// thread-local.
+/// INVARIANT: ALIGN must be a valid alignment, and no less than `value_align::<T>`.
 #[allow(missing_debug_implementations)]
-pub struct Storage<T> {
+pub struct Storage<T, const ALIGN: usize> {
     key: LazyKey,
     marker: PhantomData<Cell<T>>,
 }
 
-unsafe impl<T> Sync for Storage<T> {}
+unsafe impl<T, const ALIGN: usize> Sync for Storage<T, ALIGN> {}
 
+#[repr(C)]
 struct Value<T: 'static> {
+    // This field must be first, for correctness of `#[rustc_align_static]`
     value: T,
     // INVARIANT: if this value is stored under a TLS key, `key` must be that `key`.
     key: Key,
 }
 
-impl<T: 'static> Storage<T> {
-    pub const fn new() -> Storage<T> {
-        Storage { key: LazyKey::new(Some(destroy_value::<T>)), marker: PhantomData }
+pub const fn value_align<T: 'static>() -> usize {
+    crate::mem::align_of::<Value<T>>()
+}
+
+/// Equivalent to `Box<Value<T>>`, but potentially over-aligned.
+struct AlignedBox<T: 'static, const ALIGN: usize> {
+    ptr: NonNull<Value<T>>,
+}
+
+impl<T: 'static, const ALIGN: usize> AlignedBox<T, ALIGN> {
+    #[inline]
+    fn new(v: Value<T>) -> Self {
+        let layout = Layout::new::<Value<T>>().align_to(ALIGN).unwrap();
+
+        let ptr: *mut Value<T> = (unsafe { alloc::alloc(layout) }).cast();
+        let Some(ptr) = NonNull::new(ptr) else {
+            alloc::handle_alloc_error(layout);
+        };
+        unsafe { ptr.write(v) };
+        Self { ptr }
+    }
+
+    #[inline]
+    fn into_raw(b: Self) -> *mut Value<T> {
+        let md = ManuallyDrop::new(b);
+        md.ptr.as_ptr()
+    }
+
+    #[inline]
+    unsafe fn from_raw(ptr: *mut Value<T>) -> Self {
+        Self { ptr: unsafe { NonNull::new_unchecked(ptr) } }
+    }
+}
+
+impl<T: 'static, const ALIGN: usize> Deref for AlignedBox<T, ALIGN> {
+    type Target = Value<T>;
+
+    #[inline]
+    fn deref(&self) -> &Self::Target {
+        unsafe { &*(self.ptr.as_ptr()) }
+    }
+}
+
+impl<T: 'static, const ALIGN: usize> Drop for AlignedBox<T, ALIGN> {
+    #[inline]
+    fn drop(&mut self) {
+        let layout = Layout::new::<Value<T>>().align_to(ALIGN).unwrap();
+
+        unsafe {
+            let unwind_result = catch_unwind(AssertUnwindSafe(|| self.ptr.drop_in_place()));
+            alloc::dealloc(self.ptr.as_ptr().cast(), layout);
+            if let Err(payload) = unwind_result {
+                resume_unwind(payload);
+            }
+        }
+    }
+}
+
+impl<T: 'static, const ALIGN: usize> Storage<T, ALIGN> {
+    pub const fn new() -> Storage<T, ALIGN> {
+        Storage { key: LazyKey::new(Some(destroy_value::<T, ALIGN>)), marker: PhantomData }
     }
 
     /// Gets a pointer to the TLS value, potentially initializing it with the
@@ -95,8 +205,11 @@ impl<T: 'static> Storage<T> {
             return ptr::null();
         }
 
-        let value = Box::new(Value { value: i.and_then(Option::take).unwrap_or_else(f), key });
-        let ptr = Box::into_raw(value);
+        let value = AlignedBox::<T, ALIGN>::new(Value {
+            value: i.and_then(Option::take).unwrap_or_else(f),
+            key,
+        });
+        let ptr = AlignedBox::into_raw(value);
 
         // SAFETY:
         // * key came from a `LazyKey` and is thus correct.
@@ -114,7 +227,7 @@ impl<T: 'static> Storage<T> {
             // initializer has already returned and the next scope only starts
             // after we return the pointer. Therefore, there can be no references
             // to the old value.
-            drop(unsafe { Box::from_raw(old) });
+            drop(unsafe { AlignedBox::<T, ALIGN>::from_raw(old) });
         }
 
         // SAFETY: We just created this value above.
@@ -122,7 +235,7 @@ impl<T: 'static> Storage<T> {
     }
 }
 
-unsafe extern "C" fn destroy_value<T: 'static>(ptr: *mut u8) {
+unsafe extern "C" fn destroy_value<T: 'static, const ALIGN: usize>(ptr: *mut u8) {
     // SAFETY:
     //
     // The OS TLS ensures that this key contains a null value when this
@@ -133,7 +246,7 @@ unsafe extern "C" fn destroy_value<T: 'static>(ptr: *mut u8) {
     // Note that to prevent an infinite loop we reset it back to null right
     // before we return from the destructor ourselves.
     abort_on_dtor_unwind(|| {
-        let ptr = unsafe { Box::from_raw(ptr as *mut Value<T>) };
+        let ptr = unsafe { AlignedBox::<T, ALIGN>::from_raw(ptr as *mut Value<T>) };
         let key = ptr.key;
         // SAFETY: `key` is the TLS key `ptr` was stored under.
         unsafe { set(key, ptr::without_provenance_mut(1)) };
diff --git a/library/std/src/thread/local.rs b/library/std/src/thread/local.rs
index 0a6f2e5d508..4259a4d1f3b 100644
--- a/library/std/src/thread/local.rs
+++ b/library/std/src/thread/local.rs
@@ -132,6 +132,216 @@ impl<T: 'static> fmt::Debug for LocalKey<T> {
     }
 }
 
+#[doc(hidden)]
+#[allow_internal_unstable(thread_local_internals)]
+#[unstable(feature = "thread_local_internals", issue = "none")]
+#[rustc_macro_transparency = "semitransparent"]
+pub macro thread_local_process_attrs {
+
+    // Parse `cfg_attr` to figure out whether it's a `rustc_align_static`.
+    // Each `cfg_attr` can have zero or more attributes on the RHS, and can be nested.
+
+    // finished parsing the `cfg_attr`, it had no `rustc_align_static`
+    (
+        [] [$(#[$($prev_other_attrs:tt)*])*];
+        @processing_cfg_attr { pred: ($($predicate:tt)*), rhs: [] };
+        [$($prev_align_attrs_ret:tt)*] [$($prev_other_attrs_ret:tt)*];
+        $($rest:tt)*
+    ) => (
+        $crate::thread::local_impl::thread_local_process_attrs!(
+            [$($prev_align_attrs_ret)*] [$($prev_other_attrs_ret)* #[cfg_attr($($predicate)*, $($($prev_other_attrs)*),*)]];
+            $($rest)*
+        );
+    ),
+
+    // finished parsing the `cfg_attr`, it had nothing but `rustc_align_static`
+    (
+        [$(#[$($prev_align_attrs:tt)*])+] [];
+        @processing_cfg_attr { pred: ($($predicate:tt)*), rhs: [] };
+        [$($prev_align_attrs_ret:tt)*] [$($prev_other_attrs_ret:tt)*];
+        $($rest:tt)*
+    ) => (
+        $crate::thread::local_impl::thread_local_process_attrs!(
+            [$($prev_align_attrs_ret)*  #[cfg_attr($($predicate)*, $($($prev_align_attrs)*),+)]] [$($prev_other_attrs_ret)*];
+            $($rest)*
+        );
+    ),
+
+    // finished parsing the `cfg_attr`, it had a mix of `rustc_align_static` and other attrs
+    (
+        [$(#[$($prev_align_attrs:tt)*])+] [$(#[$($prev_other_attrs:tt)*])+];
+        @processing_cfg_attr { pred: ($($predicate:tt)*), rhs: [] };
+        [$($prev_align_attrs_ret:tt)*] [$($prev_other_attrs_ret:tt)*];
+        $($rest:tt)*
+    ) => (
+        $crate::thread::local_impl::thread_local_process_attrs!(
+            [$($prev_align_attrs_ret)*  #[cfg_attr($($predicate)*, $($($prev_align_attrs)*),+)]] [$($prev_other_attrs_ret)* #[cfg_attr($($predicate)*, $($($prev_other_attrs)*),+)]];
+            $($rest)*
+        );
+    ),
+
+    // it's a `rustc_align_static`
+    (
+        [$($prev_align_attrs:tt)*] [$($prev_other_attrs:tt)*];
+        @processing_cfg_attr { pred: ($($predicate:tt)*), rhs: [rustc_align_static($($align_static_args:tt)*) $(, $($attr_rhs:tt)*)?] };
+        $($rest:tt)*
+    ) => (
+        $crate::thread::local_impl::thread_local_process_attrs!(
+            [$($prev_align_attrs)* #[rustc_align_static($($align_static_args)*)]] [$($prev_other_attrs)*];
+            @processing_cfg_attr { pred: ($($predicate)*), rhs: [$($($attr_rhs)*)?] };
+            $($rest)*
+        );
+    ),
+
+    // it's a nested `cfg_attr(true, ...)`; recurse into RHS
+    (
+        [$($prev_align_attrs:tt)*] [$($prev_other_attrs:tt)*];
+        @processing_cfg_attr { pred: ($($predicate:tt)*), rhs: [cfg_attr(true, $($cfg_rhs:tt)*) $(, $($attr_rhs:tt)*)?] };
+        $($rest:tt)*
+    ) => (
+        $crate::thread::local_impl::thread_local_process_attrs!(
+            [] [];
+            @processing_cfg_attr { pred: (true), rhs: [$($cfg_rhs)*] };
+            [$($prev_align_attrs)*] [$($prev_other_attrs)*];
+            @processing_cfg_attr { pred: ($($predicate)*), rhs: [$($($attr_rhs)*)?] };
+            $($rest)*
+        );
+    ),
+
+    // it's a nested `cfg_attr(false, ...)`; recurse into RHS
+    (
+        [$($prev_align_attrs:tt)*] [$($prev_other_attrs:tt)*];
+        @processing_cfg_attr { pred: ($($predicate:tt)*), rhs: [cfg_attr(false, $($cfg_rhs:tt)*) $(, $($attr_rhs:tt)*)?] };
+        $($rest:tt)*
+    ) => (
+        $crate::thread::local_impl::thread_local_process_attrs!(
+            [] [];
+            @processing_cfg_attr { pred: (false), rhs: [$($cfg_rhs)*] };
+            [$($prev_align_attrs)*] [$($prev_other_attrs)*];
+            @processing_cfg_attr { pred: ($($predicate)*), rhs: [$($($attr_rhs)*)?] };
+            $($rest)*
+        );
+    ),
+
+
+    // it's a nested `cfg_attr(..., ...)`; recurse into RHS
+    (
+        [$($prev_align_attrs:tt)*] [$($prev_other_attrs:tt)*];
+        @processing_cfg_attr { pred: ($($predicate:tt)*), rhs: [cfg_attr($cfg_lhs:meta, $($cfg_rhs:tt)*) $(, $($attr_rhs:tt)*)?] };
+        $($rest:tt)*
+    ) => (
+        $crate::thread::local_impl::thread_local_process_attrs!(
+            [] [];
+            @processing_cfg_attr { pred: ($cfg_lhs), rhs: [$($cfg_rhs)*] };
+            [$($prev_align_attrs)*] [$($prev_other_attrs)*];
+            @processing_cfg_attr { pred: ($($predicate)*), rhs: [$($($attr_rhs)*)?] };
+            $($rest)*
+        );
+    ),
+
+    // it's some other attribute
+    (
+        [$($prev_align_attrs:tt)*] [$($prev_other_attrs:tt)*];
+        @processing_cfg_attr { pred: ($($predicate:tt)*), rhs: [$meta:meta $(, $($attr_rhs:tt)*)?] };
+        $($rest:tt)*
+    ) => (
+        $crate::thread::local_impl::thread_local_process_attrs!(
+            [$($prev_align_attrs)*] [$($prev_other_attrs)* #[$meta]];
+            @processing_cfg_attr { pred: ($($predicate)*), rhs: [$($($attr_rhs)*)?] };
+            $($rest)*
+        );
+    ),
+
+
+    // Separate attributes into `rustc_align_static` and everything else:
+
+    // `rustc_align_static` attribute
+    ([$($prev_align_attrs:tt)*] [$($prev_other_attrs:tt)*]; #[rustc_align_static $($attr_rest:tt)*] $($rest:tt)*) => (
+        $crate::thread::local_impl::thread_local_process_attrs!(
+            [$($prev_align_attrs)* #[rustc_align_static $($attr_rest)*]] [$($prev_other_attrs)*];
+            $($rest)*
+        );
+    ),
+
+    // `cfg_attr(true, ...)` attribute; parse it
+    ([$($prev_align_attrs:tt)*] [$($prev_other_attrs:tt)*]; #[cfg_attr(true, $($cfg_rhs:tt)*)] $($rest:tt)*) => (
+        $crate::thread::local_impl::thread_local_process_attrs!(
+            [] [];
+            @processing_cfg_attr { pred: (true), rhs: [$($cfg_rhs)*] };
+            [$($prev_align_attrs)*] [$($prev_other_attrs)*];
+            $($rest)*
+        );
+    ),
+
+    // `cfg_attr(false, ...)` attribute; parse it
+    ([$($prev_align_attrs:tt)*] [$($prev_other_attrs:tt)*]; #[cfg_attr(false, $($cfg_rhs:tt)*)] $($rest:tt)*) => (
+        $crate::thread::local_impl::thread_local_process_attrs!(
+            [] [];
+            @processing_cfg_attr { pred: (false), rhs: [$($cfg_rhs)*] };
+            [$($prev_align_attrs)*] [$($prev_other_attrs)*];
+            $($rest)*
+        );
+    ),
+
+    // `cfg_attr(..., ...)` attribute; parse it
+    ([$($prev_align_attrs:tt)*] [$($prev_other_attrs:tt)*]; #[cfg_attr($cfg_pred:meta, $($cfg_rhs:tt)*)] $($rest:tt)*) => (
+        $crate::thread::local_impl::thread_local_process_attrs!(
+            [] [];
+            @processing_cfg_attr { pred: ($cfg_pred), rhs: [$($cfg_rhs)*] };
+            [$($prev_align_attrs)*] [$($prev_other_attrs)*];
+            $($rest)*
+        );
+    ),
+
+    // doc comment not followed by any other attributes; process it all at once to avoid blowing recursion limit
+    ([$($prev_align_attrs:tt)*] [$($prev_other_attrs:tt)*]; $(#[doc $($doc_rhs:tt)*])+ $vis:vis static $($rest:tt)*) => (
+        $crate::thread::local_impl::thread_local_process_attrs!(
+            [$($prev_align_attrs)*] [$($prev_other_attrs)* $(#[doc $($doc_rhs)*])+];
+            $vis static $($rest)*
+        );
+    ),
+
+    // 8 lines of doc comment; process them all at once to avoid blowing recursion limit
+    ([$($prev_align_attrs:tt)*] [$($prev_other_attrs:tt)*];
+     #[doc $($doc_rhs_1:tt)*] #[doc $($doc_rhs_2:tt)*] #[doc $($doc_rhs_3:tt)*] #[doc $($doc_rhs_4:tt)*]
+     #[doc $($doc_rhs_5:tt)*] #[doc $($doc_rhs_6:tt)*] #[doc $($doc_rhs_7:tt)*] #[doc $($doc_rhs_8:tt)*]
+     $($rest:tt)*) => (
+        $crate::thread::local_impl::thread_local_process_attrs!(
+            [$($prev_align_attrs)*] [$($prev_other_attrs)*
+            #[doc $($doc_rhs_1)*] #[doc $($doc_rhs_2)*] #[doc $($doc_rhs_3)*] #[doc $($doc_rhs_4)*]
+            #[doc $($doc_rhs_5)*] #[doc $($doc_rhs_6)*] #[doc $($doc_rhs_7)*] #[doc $($doc_rhs_8)*]];
+            $($rest)*
+        );
+    ),
+
+    // other attribute
+    ([$($prev_align_attrs:tt)*] [$($prev_other_attrs:tt)*]; #[$($attr:tt)*] $($rest:tt)*) => (
+        $crate::thread::local_impl::thread_local_process_attrs!(
+            [$($prev_align_attrs)*] [$($prev_other_attrs)* #[$($attr)*]];
+            $($rest)*
+        );
+    ),
+
+
+    // Delegate to `thread_local_inner` once attributes are fully categorized:
+
+    // process `const` declaration and recurse
+    ([$($align_attrs:tt)*] [$($other_attrs:tt)*]; $vis:vis static $name:ident: $t:ty = const $init:block $(; $($($rest:tt)+)?)?) => (
+        $($other_attrs)* $vis const $name: $crate::thread::LocalKey<$t> =
+            $crate::thread::local_impl::thread_local_inner!(@key $t, $($align_attrs)*, const $init);
+
+        $($($crate::thread::local_impl::thread_local_process_attrs!([] []; $($rest)+);)?)?
+    ),
+
+    // process non-`const` declaration and recurse
+    ([$($align_attrs:tt)*] [$($other_attrs:tt)*]; $vis:vis static $name:ident: $t:ty = $init:expr $(; $($($rest:tt)+)?)?) => (
+        $($other_attrs)* $vis const $name: $crate::thread::LocalKey<$t> =
+            $crate::thread::local_impl::thread_local_inner!(@key $t, $($align_attrs)*, $init);
+
+        $($($crate::thread::local_impl::thread_local_process_attrs!([] []; $($rest)+);)?)?
+    ),
+}
+
 /// Declare a new thread local storage key of type [`std::thread::LocalKey`].
 ///
 /// # Syntax
@@ -182,28 +392,11 @@ impl<T: 'static> fmt::Debug for LocalKey<T> {
 #[cfg_attr(not(test), rustc_diagnostic_item = "thread_local_macro")]
 #[allow_internal_unstable(thread_local_internals)]
 macro_rules! thread_local {
-    // empty (base case for the recursion)
     () => {};
 
-    ($(#[$attr:meta])* $vis:vis static $name:ident: $t:ty = const $init:block; $($rest:tt)*) => (
-        $crate::thread::local_impl::thread_local_inner!($(#[$attr])* $vis $name, $t, const $init);
-        $crate::thread_local!($($rest)*);
-    );
-
-    ($(#[$attr:meta])* $vis:vis static $name:ident: $t:ty = const $init:block) => (
-        $crate::thread::local_impl::thread_local_inner!($(#[$attr])* $vis $name, $t, const $init);
-    );
-
-    // process multiple declarations
-    ($(#[$attr:meta])* $vis:vis static $name:ident: $t:ty = $init:expr; $($rest:tt)*) => (
-        $crate::thread::local_impl::thread_local_inner!($(#[$attr])* $vis $name, $t, $init);
-        $crate::thread_local!($($rest)*);
-    );
-
-    // handle a single declaration
-    ($(#[$attr:meta])* $vis:vis static $name:ident: $t:ty = $init:expr) => (
-        $crate::thread::local_impl::thread_local_inner!($(#[$attr])* $vis $name, $t, $init);
-    );
+    ($($tt:tt)+) => {
+        $crate::thread::local_impl::thread_local_process_attrs!([] []; $($tt)+);
+    };
 }
 
 /// An error returned by [`LocalKey::try_with`](struct.LocalKey.html#method.try_with).
diff --git a/library/std/src/thread/mod.rs b/library/std/src/thread/mod.rs
index 1768369792a..fd7cce3f97d 100644
--- a/library/std/src/thread/mod.rs
+++ b/library/std/src/thread/mod.rs
@@ -207,6 +207,7 @@ pub use self::local::{AccessError, LocalKey};
 #[doc(hidden)]
 #[unstable(feature = "thread_local_internals", issue = "none")]
 pub mod local_impl {
+    pub use super::local::thread_local_process_attrs;
     pub use crate::sys::thread_local::*;
 }
 
diff --git a/library/std/tests/thread.rs b/library/std/tests/thread.rs
index 29f220d8a70..dc8eadd7514 100644
--- a/library/std/tests/thread.rs
+++ b/library/std/tests/thread.rs
@@ -66,6 +66,8 @@ fn thread_local_hygeiene() {
     type Storage = ();
     type LazyStorage = ();
     type EagerStorage = ();
+    #[allow(non_camel_case_types)]
+    type usize = ();
     thread_local! {
         static A: LocalKey = const { () };
         static B: Storage = const { () };
diff --git a/src/tools/miri/tests/pass/static_align.rs b/src/tools/miri/tests/pass/static_align.rs
index f292f028568..bc6a9bf8af7 100644
--- a/src/tools/miri/tests/pass/static_align.rs
+++ b/src/tools/miri/tests/pass/static_align.rs
@@ -1,4 +1,7 @@
 #![feature(static_align)]
+#![deny(non_upper_case_globals)]
+
+use std::cell::Cell;
 
 // When a static uses `align(N)`, its address should be a multiple of `N`.
 
@@ -8,7 +11,64 @@ static FOO: u64 = 0;
 #[rustc_align_static(512)]
 static BAR: u64 = 0;
 
+struct HasDrop(*const HasDrop);
+
+impl Drop for HasDrop {
+    fn drop(&mut self) {
+        assert_eq!(core::ptr::from_mut(self).cast_const(), self.0);
+    }
+}
+
+thread_local! {
+    #[rustc_align_static(4096)]
+    static LOCAL: u64 = 0;
+
+    #[allow(unused_mut, reason = "test attribute handling")]
+    #[cfg_attr(true, rustc_align_static(4096))]
+    static CONST_LOCAL: u64 = const { 0 };
+
+    #[cfg_attr(any(true), cfg_attr(true, rustc_align_static(4096)))]
+    #[allow(unused_mut, reason = "test attribute handling")]
+    static HASDROP_LOCAL: Cell<HasDrop> = Cell::new(HasDrop(core::ptr::null()));
+
+    /// I love doc comments.
+    #[allow(unused_mut, reason = "test attribute handling")]
+    #[cfg_attr(all(),
+      cfg_attr(any(true),
+      cfg_attr(true, rustc_align_static(4096))))]
+    #[allow(unused_mut, reason = "test attribute handling")]
+    /// I love doc comments.
+    static HASDROP_CONST_LOCAL: Cell<HasDrop> = const { Cell::new(HasDrop(core::ptr::null())) };
+
+    #[cfg_attr(true,)]
+    #[cfg_attr(false,)]
+    #[cfg_attr(
+        true,
+        rustc_align_static(32),
+        cfg_attr(true, allow(non_upper_case_globals, reason = "test attribute handling")),
+        cfg_attr(false,)
+    )]
+    #[cfg_attr(false, rustc_align_static(0))]
+    static more_attr_testing: u64 = 0;
+}
+
+fn thread_local_ptr<T>(key: &'static std::thread::LocalKey<T>) -> *const T {
+    key.with(|local| core::ptr::from_ref::<T>(local))
+}
+
 fn main() {
     assert!(core::ptr::from_ref(&FOO).addr().is_multiple_of(256));
     assert!(core::ptr::from_ref(&BAR).addr().is_multiple_of(512));
+
+    assert!(thread_local_ptr(&LOCAL).addr().is_multiple_of(4096));
+    assert!(thread_local_ptr(&CONST_LOCAL).addr().is_multiple_of(4096));
+    assert!(thread_local_ptr(&HASDROP_LOCAL).addr().is_multiple_of(4096));
+    assert!(thread_local_ptr(&HASDROP_CONST_LOCAL).addr().is_multiple_of(4096));
+    assert!(thread_local_ptr(&more_attr_testing).addr().is_multiple_of(32));
+
+    // Test that address (and therefore alignment) is maintained during drop
+    let hasdrop_ptr = thread_local_ptr(&HASDROP_LOCAL);
+    core::mem::forget(HASDROP_LOCAL.replace(HasDrop(hasdrop_ptr.cast())));
+    let hasdrop_const_ptr = thread_local_ptr(&HASDROP_CONST_LOCAL);
+    core::mem::forget(HASDROP_CONST_LOCAL.replace(HasDrop(hasdrop_const_ptr.cast())));
 }
diff --git a/src/tools/miri/tests/pass/thread_local-panic.rs b/src/tools/miri/tests/pass/thread_local-panic.rs
new file mode 100644
index 00000000000..549acd23987
--- /dev/null
+++ b/src/tools/miri/tests/pass/thread_local-panic.rs
@@ -0,0 +1,8 @@
+thread_local! {
+    static LOCAL: u64 = panic!();
+
+}
+
+fn main() {
+    let _ = std::panic::catch_unwind(|| LOCAL.with(|_| {}));
+}
diff --git a/src/tools/miri/tests/pass/thread_local-panic.stderr b/src/tools/miri/tests/pass/thread_local-panic.stderr
new file mode 100644
index 00000000000..e69340a8102
--- /dev/null
+++ b/src/tools/miri/tests/pass/thread_local-panic.stderr
@@ -0,0 +1,5 @@
+
+thread 'main' ($TID) panicked at tests/pass/thread_local-panic.rs:LL:CC:
+explicit panic
+note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
+note: in Miri, you may have to set `MIRIFLAGS=-Zmiri-env-forward=RUST_BACKTRACE` for the environment variable to have an effect
diff --git a/tests/ui/feature-gates/feature-gate-static_align-thread_local.rs b/tests/ui/feature-gates/feature-gate-static_align-thread_local.rs
new file mode 100644
index 00000000000..29d4facffce
--- /dev/null
+++ b/tests/ui/feature-gates/feature-gate-static_align-thread_local.rs
@@ -0,0 +1,11 @@
+// The feature gate error may be emitted twice, but only on certain targets
+//@ dont-require-annotations: ERROR
+//@ dont-check-compiler-stderr
+
+#![crate_type = "lib"]
+
+thread_local! {
+    //~^ ERROR the `#[rustc_align_static]` attribute is an experimental feature
+    #[rustc_align_static(16)]
+    static THREAD_LOCAL: u16 = 0;
+}
diff --git a/tests/ui/macros/macro-local-data-key-priv.stderr b/tests/ui/macros/macro-local-data-key-priv.stderr
index e93bd11046d..8df1aec140d 100644
--- a/tests/ui/macros/macro-local-data-key-priv.stderr
+++ b/tests/ui/macros/macro-local-data-key-priv.stderr
@@ -9,7 +9,7 @@ note: the constant `baz` is defined here
    |
 LL |     thread_local!(static baz: f64 = 0.0);
    |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
-   = note: this error originates in the macro `$crate::thread::local_impl::thread_local_inner` which comes from the expansion of the macro `thread_local` (in Nightly builds, run with -Z macro-backtrace for more info)
+   = note: this error originates in the macro `$crate::thread::local_impl::thread_local_process_attrs` which comes from the expansion of the macro `thread_local` (in Nightly builds, run with -Z macro-backtrace for more info)
 
 error: aborting due to 1 previous error
 
diff --git a/tests/ui/static/static-align.rs b/tests/ui/static/static-align.rs
index 93241db09f9..e2db7c01adf 100644
--- a/tests/ui/static/static-align.rs
+++ b/tests/ui/static/static-align.rs
@@ -1,10 +1,14 @@
 //@ run-pass
+//@ compile-flags: --cfg FOURTY_TWO="42" --cfg TRUE --check-cfg=cfg(FOURTY_TWO,values("42")) --check-cfg=cfg(TRUE)
 #![feature(static_align)]
+#![deny(non_upper_case_globals)]
+
+use std::cell::Cell;
 
 #[rustc_align_static(64)]
 static A: u8 = 0;
 
-#[rustc_align_static(64)]
+#[rustc_align_static(4096)]
 static B: u8 = 0;
 
 #[rustc_align_static(128)]
@@ -17,10 +21,86 @@ unsafe extern "C" {
     static C: u64;
 }
 
+struct HasDrop(*const HasDrop);
+
+impl Drop for HasDrop {
+    fn drop(&mut self) {
+        assert_eq!(core::ptr::from_mut(self).cast_const(), self.0);
+    }
+}
+
+thread_local! {
+    #[rustc_align_static(4096)]
+    static LOCAL: u64 = 0;
+
+    #[allow(unused_mut, reason = "test attribute handling")]
+    #[cfg_attr(true, rustc_align_static(4096))]
+    static CONST_LOCAL: u64 = const { 0 };
+
+    #[cfg_attr(any(true), cfg_attr(true, rustc_align_static(4096)))]
+    #[allow(unused_mut, reason = "test attribute handling")]
+    static HASDROP_LOCAL: Cell<HasDrop> = Cell::new(HasDrop(core::ptr::null()));
+
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    #[allow(unused_mut, reason = "test attribute handling")]
+    #[cfg_attr(TRUE,
+      cfg_attr(FOURTY_TWO = "42",
+      cfg_attr(all(),
+      cfg_attr(any(true),
+      cfg_attr(true, rustc_align_static(4096))))))]
+    #[allow(unused_mut, reason = "test attribute handling")]
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    static HASDROP_CONST_LOCAL: Cell<HasDrop> = const { Cell::new(HasDrop(core::ptr::null())) };
+
+    #[cfg_attr(TRUE,)]
+    #[cfg_attr(true,)]
+    #[cfg_attr(false,)]
+    #[cfg_attr(
+        TRUE,
+        rustc_align_static(32),
+        cfg_attr(true, allow(non_upper_case_globals, reason = "test attribute handling")),
+        cfg_attr(false,)
+    )]
+    #[cfg_attr(false, rustc_align_static(0))]
+    static more_attr_testing: u64 = 0;
+}
+
+fn thread_local_ptr<T>(key: &'static std::thread::LocalKey<T>) -> *const T {
+    key.with(|local| core::ptr::from_ref::<T>(local))
+}
+
 fn main() {
     assert!(core::ptr::from_ref(&A).addr().is_multiple_of(64));
-    assert!(core::ptr::from_ref(&B).addr().is_multiple_of(64));
+    assert!(core::ptr::from_ref(&B).addr().is_multiple_of(4096));
 
     assert!(core::ptr::from_ref(&EXPORTED).addr().is_multiple_of(128));
     unsafe { assert!(core::ptr::from_ref(&C).addr().is_multiple_of(128)) };
+
+    assert!(thread_local_ptr(&LOCAL).addr().is_multiple_of(4096));
+    assert!(thread_local_ptr(&CONST_LOCAL).addr().is_multiple_of(4096));
+    assert!(thread_local_ptr(&HASDROP_LOCAL).addr().is_multiple_of(4096));
+    assert!(thread_local_ptr(&HASDROP_CONST_LOCAL).addr().is_multiple_of(4096));
+    assert!(thread_local_ptr(&more_attr_testing).addr().is_multiple_of(32));
+
+    // Test that address (and therefore alignment) is maintained during drop
+    let hasdrop_ptr = thread_local_ptr(&HASDROP_LOCAL);
+    core::mem::forget(HASDROP_LOCAL.replace(HasDrop(hasdrop_ptr.cast())));
+    let hasdrop_const_ptr = thread_local_ptr(&HASDROP_CONST_LOCAL);
+    core::mem::forget(HASDROP_CONST_LOCAL.replace(HasDrop(hasdrop_const_ptr.cast())));
 }
diff --git a/tests/ui/thread-local/long-docs.rs b/tests/ui/thread-local/long-docs.rs
new file mode 100644
index 00000000000..0577d0b27c2
--- /dev/null
+++ b/tests/ui/thread-local/long-docs.rs
@@ -0,0 +1,266 @@
+//@ check-pass
+
+thread_local! {
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    pub static LONG_DOCS: () = ();
+
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    /// I love doc comments.
+    #[allow(unused_mut, reason = "testing")]
+    pub static LONG_DOCS_2: () = ();
+}
+
+fn main() {}
diff --git a/tests/ui/thread-local/no-unstable.rs b/tests/ui/thread-local/no-unstable.rs
new file mode 100644
index 00000000000..3de7985e62d
--- /dev/null
+++ b/tests/ui/thread-local/no-unstable.rs
@@ -0,0 +1,17 @@
+thread_local! {
+    //~^ ERROR: use of an internal attribute [E0658]
+    //~| ERROR: use of an internal attribute [E0658]
+    //~| ERROR: `#[used(linker)]` is currently unstable [E0658]
+    //~| ERROR: `#[used]` attribute cannot be used on constants
+
+    #[rustc_dummy = 17]
+    pub static FOO: () = ();
+
+    #[cfg_attr(true, rustc_dummy = 17)]
+    pub static BAR: () = ();
+
+    #[used(linker)]
+    pub static BAZ: () = ();
+}
+
+fn main() {}
diff --git a/tests/ui/thread-local/no-unstable.stderr b/tests/ui/thread-local/no-unstable.stderr
new file mode 100644
index 00000000000..fbcd804d917
--- /dev/null
+++ b/tests/ui/thread-local/no-unstable.stderr
@@ -0,0 +1,57 @@
+error[E0658]: use of an internal attribute
+  --> $DIR/no-unstable.rs:1:1
+   |
+LL | / thread_local! {
+...  |
+LL | |     pub static BAZ: () = ();
+LL | | }
+   | |_^
+   |
+   = help: add `#![feature(rustc_attrs)]` to the crate attributes to enable
+   = note: the `#[rustc_dummy]` attribute is an internal implementation detail that will never be stable
+   = note: the `#[rustc_dummy]` attribute is used for rustc unit tests
+   = note: this error originates in the macro `$crate::thread::local_impl::thread_local_process_attrs` which comes from the expansion of the macro `thread_local` (in Nightly builds, run with -Z macro-backtrace for more info)
+
+error[E0658]: use of an internal attribute
+  --> $DIR/no-unstable.rs:1:1
+   |
+LL | / thread_local! {
+...  |
+LL | |     pub static BAZ: () = ();
+LL | | }
+   | |_^
+   |
+   = help: add `#![feature(rustc_attrs)]` to the crate attributes to enable
+   = note: the `#[rustc_dummy]` attribute is an internal implementation detail that will never be stable
+   = note: the `#[rustc_dummy]` attribute is used for rustc unit tests
+   = note: this error originates in the macro `$crate::thread::local_impl::thread_local_process_attrs` which comes from the expansion of the macro `thread_local` (in Nightly builds, run with -Z macro-backtrace for more info)
+
+error[E0658]: `#[used(linker)]` is currently unstable
+  --> $DIR/no-unstable.rs:1:1
+   |
+LL | / thread_local! {
+...  |
+LL | |     pub static BAZ: () = ();
+LL | | }
+   | |_^
+   |
+   = note: see issue #93798 <https://github.com/rust-lang/rust/issues/93798> for more information
+   = help: add `#![feature(used_with_arg)]` to the crate attributes to enable
+   = note: this compiler was built on YYYY-MM-DD; consider upgrading it if it is out of date
+   = note: this error originates in the macro `$crate::thread::local_impl::thread_local_process_attrs` which comes from the expansion of the macro `thread_local` (in Nightly builds, run with -Z macro-backtrace for more info)
+
+error: `#[used]` attribute cannot be used on constants
+  --> $DIR/no-unstable.rs:1:1
+   |
+LL | / thread_local! {
+...  |
+LL | |     pub static BAZ: () = ();
+LL | | }
+   | |_^
+   |
+   = help: `#[used]` can only be applied to statics
+   = note: this error originates in the macro `$crate::thread::local_impl::thread_local_process_attrs` which comes from the expansion of the macro `thread_local` (in Nightly builds, run with -Z macro-backtrace for more info)
+
+error: aborting due to 4 previous errors
+
+For more information about this error, try `rustc --explain E0658`.