Source: rust-tracing-core
Version: 0.1.36-1
Severity: normal
Tags: patch upstream

Dear rust-tracing-core maintainers,

Currently, rust-tracing-core assumes that Rust niche optimization[1]
remains the
same, even though Rust makes no such guarantee. As of Rust 1.97, this
hardcoded
assumption is no longer true, meaning that the
metadata::tests::level_filter_reprs
test will break[2]:

167s ---- metadata::tests::level_filter_reprs stdout ----
167s
167s thread 'metadata::tests::level_filter_reprs' (5315) panicked at
src/metadata.rs:1119:13:
167s assertion `left == right` failed: repr changed for LevelFilter::OFF
167s left: 5
167s right: 18446744073709551615
167s stack backtrace:
167s 0: __rustc::rust_begin_unwind
167s at /usr/src/rustc-1.97.1/library/std/src/panicking.rs:689:5
167s 1: core::panicking::panic_fmt
167s at /usr/src/rustc-1.97.1/library/core/src/panicking.rs:80:14
167s 2: core::panicking::assert_failed_inner
167s at /usr/src/rustc-1.97.1/library/core/src/panicking.rs:434:23
167s 3: core::panicking::assert_failed::<usize, usize>
167s at /usr/src/rustc-1.97.1/library/core/src/panicking.rs:394:5
167s 4: tracing_core::metadata::tests::level_filter_reprs
167s at ./src/metadata.rs:1119:13
167s 5: tracing_core::metadata::tests::level_filter_reprs::{closure#0}
167s at ./src/metadata.rs:1102:28
167s 6: <tracing_core::metadata::tests::level_filter_reprs::{closure#0} as
core::ops::function::FnOnce<()>>::call_once
167s at /usr/src/rustc-1.97.1/library/core/src/ops/function.rs:250:5
167s 7: <fn() -> core::result::Result<(), alloc::string::String> as
core::ops::function::FnOnce<()>>::call_once
167s at /usr/src/rustc-1.97.1/library/core/src/ops/function.rs:250:5
167s note: Some details are omitted, run with `RUST_BACKTRACE=full` for a
verbose backtrace.

Naturally, this bug will not impact Debian until Rust is updated to 1.97.
However, it is affecting Ubuntu and being tracked as LP: #2167368.

I have attached a patch which cherry-picks the upstream fix[3]. The fix
makes it so the crate works with all Rust versions past the MSRV -- I
successfully ran all tests with both 1.93.1 and 1.97.1, and I verified that
the package builds against Debian unstable.

Please let me know if you need anything else.

Best wishes,

Max

[1]: https://www.0xatticus.com/posts/understanding_rust_niche/
[2]:
https://autopkgtest.ubuntu.com/results/autopkgtest-stonking/stonking/amd64/r/rust-tracing-core/20260912_123714_a8bc3@/log.gz

[3]: https://github.com/tokio-rs/tracing/pull/3537
Description: Get LevelFilter::OFF niche representation dynamically
 Determining the value for `Option::None` at compile time via a union
 transmute ensures that we always match the exact representation
 selected by the compiler's niche optimization for `LevelFilter` on any
 Rust version. The niche optimization changed in Rust 1.97.
 .
 This patch has been cherry-picked from upstream to unblock the Rust
 1.97 transition. It can be dropped once we get an upstream tracing-core
 version which contains the fix.
Author: Augie Fackler <[email protected]>
Origin: upstream, https://github.com/tokio-rs/tracing/pull/3537.diff
Bug: https://github.com/tokio-rs/tracing/pull/3537
Bug-Ubuntu: https://bugs.launchpad.net/bugs/2167368
---
--- a/src/metadata.rs
+++ b/src/metadata.rs
@@ -677,12 +677,13 @@
     const INFO_USIZE: usize = LevelInner::Info as usize;
     const DEBUG_USIZE: usize = LevelInner::Debug as usize;
     const TRACE_USIZE: usize = LevelInner::Trace as usize;
-    // Using the value of the last variant + 1 ensures that we match the value
-    // for `Option::None` as selected by the niche optimization for
-    // `LevelFilter`. If this is the case, converting a `usize` value into a
-    // `LevelFilter` (in `LevelFilter::current`) will be an identity conversion,
-    // rather than generating a lookup table.
-    const OFF_USIZE: usize = LevelInner::Error as usize + 1;
+    // Dynamically determining the value for `Option::None` at compile time
+    // via `core::mem::transmute` ensures that we always match the exact representation
+    // selected by the compiler's niche optimization for `LevelFilter` on any
+    // Rust version. This guarantees that converting a `usize` value into a
+    // `LevelFilter` (in `LevelFilter::current`) is always a zero-cost identity
+    // conversion, rather than generating a lookup table.
+    const OFF_USIZE: usize = unsafe { core::mem::transmute(LevelFilter::OFF) };
 
     /// Returns a `LevelFilter` that matches the most verbose [`Level`] that any
     /// currently active [`Subscriber`] will enable.
@@ -886,11 +887,10 @@
 //    `Option<Level>`) compiles down to a single integer value. This is
 //    necessary for storing the global max in an `AtomicUsize`, and for ensuring
 //    that we use fast integer-integer comparisons, as mentioned previously. In
-//    order to ensure this, we exploit the niche optimization. The niche
-//    optimization for `Option<{enum with a numeric repr}>` will choose
-//    `(HIGHEST_DISCRIMINANT_VALUE + 1)` as the representation for `None`.
-//    Therefore, the integer representation of `LevelFilter::OFF` (which is
-//    `None`) will be the number 5. `OFF` must compare higher than every other
+//    order to ensure this, we exploit the niche optimization. The exact raw
+//    representation selected by `rustc` for `Option::None` may vary across
+//    compiler versions, so `LevelFilter::OFF_USIZE` is determined dynamically
+//    via `core::mem::transmute`. `OFF` must compare higher than every other
 //    level in order for it to filter as expected. Since we want to use a single
 //    `cmp` instruction, we can't special-case the integer value of `OFF` to
 //    compare higher, as that will generate more code. Instead, we need it to be
@@ -911,7 +911,7 @@
 impl PartialEq<LevelFilter> for Level {
     #[inline(always)]
     fn eq(&self, other: &LevelFilter) -> bool {
-        self.0 as usize == filter_as_usize(&other.0)
+        self.0 as usize == filter_as_usize_sort_key(other.0)
     }
 }
 
@@ -952,42 +952,46 @@
 impl PartialOrd<LevelFilter> for Level {
     #[inline(always)]
     fn partial_cmp(&self, other: &LevelFilter) -> Option<cmp::Ordering> {
-        Some(filter_as_usize(&other.0).cmp(&(self.0 as usize)))
+        Some(filter_as_usize_sort_key(other.0).cmp(&(self.0 as usize)))
     }
 
     #[inline(always)]
     fn lt(&self, other: &LevelFilter) -> bool {
-        filter_as_usize(&other.0) < (self.0 as usize)
+        filter_as_usize_sort_key(other.0) < (self.0 as usize)
     }
 
     #[inline(always)]
     fn le(&self, other: &LevelFilter) -> bool {
-        filter_as_usize(&other.0) <= (self.0 as usize)
+        filter_as_usize_sort_key(other.0) <= (self.0 as usize)
     }
 
     #[inline(always)]
     fn gt(&self, other: &LevelFilter) -> bool {
-        filter_as_usize(&other.0) > (self.0 as usize)
+        filter_as_usize_sort_key(other.0) > (self.0 as usize)
     }
 
     #[inline(always)]
     fn ge(&self, other: &LevelFilter) -> bool {
-        filter_as_usize(&other.0) >= (self.0 as usize)
+        filter_as_usize_sort_key(other.0) >= (self.0 as usize)
     }
 }
 
+const USE_NATIVE_SORT: bool = LevelFilter::OFF_USIZE > LevelInner::Error as usize;
+
 #[inline(always)]
-fn filter_as_usize(x: &Option<Level>) -> usize {
+fn filter_as_usize_sort_key(x: Option<Level>) -> usize {
     match x {
-        Some(Level(f)) => *f as usize,
-        None => LevelFilter::OFF_USIZE,
+        Some(Level(f)) => f as usize,
+        // The niche optimization for LevelFilter::OFF isn't guaranteed
+        // to be the last variant + 1, so we explicitly return that for sorting here.
+        None => if USE_NATIVE_SORT { LevelFilter::OFF_USIZE } else { LevelInner::Error as usize + 1 },
     }
 }
 
 impl PartialEq<Level> for LevelFilter {
     #[inline(always)]
     fn eq(&self, other: &Level) -> bool {
-        filter_as_usize(&self.0) == other.0 as usize
+        filter_as_usize_sort_key(self.0) == other.0 as usize
     }
 }
 
@@ -999,56 +1003,56 @@
 
     #[inline(always)]
     fn lt(&self, other: &LevelFilter) -> bool {
-        filter_as_usize(&other.0) < filter_as_usize(&self.0)
+        filter_as_usize_sort_key(other.0) < filter_as_usize_sort_key(self.0)
     }
 
     #[inline(always)]
     fn le(&self, other: &LevelFilter) -> bool {
-        filter_as_usize(&other.0) <= filter_as_usize(&self.0)
+        filter_as_usize_sort_key(other.0) <= filter_as_usize_sort_key(self.0)
     }
 
     #[inline(always)]
     fn gt(&self, other: &LevelFilter) -> bool {
-        filter_as_usize(&other.0) > filter_as_usize(&self.0)
+        filter_as_usize_sort_key(other.0) > filter_as_usize_sort_key(self.0)
     }
 
     #[inline(always)]
     fn ge(&self, other: &LevelFilter) -> bool {
-        filter_as_usize(&other.0) >= filter_as_usize(&self.0)
+        filter_as_usize_sort_key(other.0) >= filter_as_usize_sort_key(self.0)
     }
 }
 
 impl Ord for LevelFilter {
     #[inline(always)]
     fn cmp(&self, other: &Self) -> cmp::Ordering {
-        filter_as_usize(&other.0).cmp(&filter_as_usize(&self.0))
+        filter_as_usize_sort_key(other.0).cmp(&filter_as_usize_sort_key(self.0))
     }
 }
 
 impl PartialOrd<Level> for LevelFilter {
     #[inline(always)]
     fn partial_cmp(&self, other: &Level) -> Option<cmp::Ordering> {
-        Some((other.0 as usize).cmp(&filter_as_usize(&self.0)))
+        Some((other.0 as usize).cmp(&filter_as_usize_sort_key(self.0)))
     }
 
     #[inline(always)]
     fn lt(&self, other: &Level) -> bool {
-        (other.0 as usize) < filter_as_usize(&self.0)
+        (other.0 as usize) < filter_as_usize_sort_key(self.0)
     }
 
     #[inline(always)]
     fn le(&self, other: &Level) -> bool {
-        (other.0 as usize) <= filter_as_usize(&self.0)
+        (other.0 as usize) <= filter_as_usize_sort_key(self.0)
     }
 
     #[inline(always)]
     fn gt(&self, other: &Level) -> bool {
-        (other.0 as usize) > filter_as_usize(&self.0)
+        (other.0 as usize) > filter_as_usize_sort_key(self.0)
     }
 
     #[inline(always)]
     fn ge(&self, other: &Level) -> bool {
-        (other.0 as usize) >= filter_as_usize(&self.0)
+        (other.0 as usize) >= filter_as_usize_sort_key(self.0)
     }
 }
 
@@ -1101,7 +1105,7 @@
     #[test]
     fn level_filter_reprs() {
         let mapping = [
-            (LevelFilter::OFF, LevelInner::Error as usize + 1),
+            (LevelFilter::OFF, LevelFilter::OFF_USIZE),
             (LevelFilter::ERROR, LevelInner::Error as usize),
             (LevelFilter::WARN, LevelInner::Warn as usize),
             (LevelFilter::INFO, LevelInner::Info as usize),
@@ -1119,4 +1123,41 @@
             assert_eq!(expected, repr, "repr changed for {:?}", filter)
         }
     }
+
+    #[test]
+    fn level_filter_ordering() {
+        assert!(LevelFilter::OFF < LevelFilter::ERROR);
+        assert!(LevelFilter::OFF < LevelFilter::WARN);
+        assert!(LevelFilter::OFF < LevelFilter::INFO);
+        assert!(LevelFilter::OFF < LevelFilter::DEBUG);
+        assert!(LevelFilter::OFF < LevelFilter::TRACE);
+
+        assert!(LevelFilter::OFF < Level::ERROR);
+        assert!(LevelFilter::OFF < Level::WARN);
+        assert!(LevelFilter::OFF < Level::INFO);
+        assert!(LevelFilter::OFF < Level::DEBUG);
+        assert!(LevelFilter::OFF < Level::TRACE);
+
+        assert!(LevelFilter::ERROR < LevelFilter::WARN);
+        assert!(LevelFilter::WARN < LevelFilter::INFO);
+        assert!(LevelFilter::INFO < LevelFilter::DEBUG);
+        assert!(LevelFilter::DEBUG < LevelFilter::TRACE);
+
+        assert!(Level::ERROR < LevelFilter::WARN);
+        assert!(Level::WARN < LevelFilter::INFO);
+        assert!(Level::INFO < LevelFilter::DEBUG);
+        assert!(Level::DEBUG < LevelFilter::TRACE);
+
+        assert!(LevelFilter::ERROR > LevelFilter::OFF);
+        assert!(LevelFilter::WARN > LevelFilter::OFF);
+        assert!(LevelFilter::INFO > LevelFilter::OFF);
+        assert!(LevelFilter::DEBUG > LevelFilter::OFF);
+        assert!(LevelFilter::TRACE > LevelFilter::OFF);
+
+        assert!(Level::ERROR > LevelFilter::OFF);
+        assert!(Level::WARN > LevelFilter::OFF);
+        assert!(Level::INFO > LevelFilter::OFF);
+        assert!(Level::DEBUG > LevelFilter::OFF);
+        assert!(Level::TRACE > LevelFilter::OFF);
+    }
 }

Reply via email to