cargo : tracing-subscriber @ 0.3.23
PE Patrick Elsen signed 2026-05-28 published 2026-05-28

Claims

concurrency-documentedconcurrency-safeenvironment-safehas-binarieshas-build-exechas-fuzz-testshas-install-exechas-integration-testshas-property-testshas-unit-testsimpl-algorithmimpl-concurrencyimpl-cryptoimpl-datastructureimpl-interpreterimpl-jitimpl-parserimpl-protocolis-benignparser-impl-correctparser-impl-safeparser-impl-testedunsafe-documentedunsafe-minimalunsafe-safeunsafe-testeduses-concurrencyuses-cryptouses-environmentuses-execuses-filesystemuses-interpreteruses-jituses-networkuses-unsafe

Summary

tracing-subscriber 0.3.23 implements the Layer composition API, span Registry (sharded slab), EnvFilter directive parser, and fmt output layers for the tracing ecosystem. All 25 unsafe blocks implement downcast_raw with documented invariants; no pointers are dereferenced beyond type-checked reference casts. One low-severity quality finding: regex field-value matching in EnvFilter is enabled by default, presenting a ReDoS surface if filter strings come from untrusted inputs.

Report

Subject

tracing-subscriber 0.3.23 implements the collector-side of the tracing ecosystem. It exposes the Layer composition API for building Subscriber implementations from modular pieces, a thread-sharded span registry (Registry), an environment-variable-driven filter (EnvFilter), and formatted output layers (fmt::Layer) with multiple output formats including compact, pretty, JSON, and ANSI-colored terminal output. It is a library crate with no binaries or build scripts.

Methodology

The published crate contents were compared against the upstream VCS checkout at the commit recorded in .cargo_vcs_info.json using diff -rq. All 40 source files (~23K LOC) were surveyed with grep for unsafe blocks, FFI declarations, network, filesystem, process, environment, crypto, and concurrency patterns. All files containing unsafe (25 occurrences across 7 files), the directive parser (src/filter/env/directive.rs, src/filter/env/field.rs), the span registry (src/registry/sharded.rs, src/registry/extensions.rs), the reload wrapper (src/reload.rs), and the layer composition types (src/layer/layered.rs, src/layer/mod.rs, src/filter/layer_filters/mod.rs) were read in full. Supporting files (src/sync.rs, src/fmt/format/escape.rs, src/fmt/time/datetime.rs) were also read in full. Tools used: openvet 0.6.0, diff.

Results

The VCS comparison shows no source-code differences between the published crate and the upstream repository. The only differences are Cargo.toml (cargo normalisation), Cargo.toml.orig, .cargo_vcs_info.json, and Cargo.lock, all expected. The crate carries no binary artifacts, no build script, and no install-time hooks, justifying has-binaries, has-build-exec, has-install-exec. The package ships 261 unit and integration tests across src/ and tests/ (has-unit-tests, has-integration-tests). No fuzz tests or property tests are present (has-fuzz-tests, has-property-tests).

The 25 unsafe occurrences fall into two patterns. Twenty-three sites implement the Subscriber::downcast_raw unsafe method, which returns a raw pointer to self or an inner field when the requested TypeId matches. No pointer is ever dereferenced beyond the TypeId check: the marker-type pattern (NoneLayerMarker, MagicPlfDowncastMarker) uses the pointer as a boolean signal, and the downcast_ref wrapper dereferences only after a null check. Three helper functions (layer_is_none, subscriber_is_none, layer_has_plf) call downcast_raw to probe for marker types and explicitly document in their safety comments that the resulting pointer is never dereferenced. All unsafe blocks carry // Safety: comments. Justifies uses-unsafe, unsafe-safe, unsafe-documented, unsafe-minimal. Miri and ASAN are not documented as part of the upstream CI for this crate, so unsafe-tested is asserted false.

The crate does not read from or write to the filesystem directly, does not open network sockets, does not spawn child processes, and does not use cryptographic operations, justifying uses-network, uses-filesystem, uses-exec, uses-crypto. The crate implements no JIT compiler, interpreter, network protocol, standalone data structure, cryptographic algorithm, or concurrency primitive, justifying impl-jit, impl-interpreter, impl-protocol, impl-datastructure, impl-algorithm, impl-concurrency, impl-crypto. The EnvFilter builder reads std::env::var("RUST_LOG") (or a caller-configured variable name) and no other environment variables; it does not enumerate the environment, justifying uses-environment and environment-safe. uses-jit and uses-interpreter are false for the same reasons.

Concurrency is handled through RwLock (either std::sync::RwLock or parking_lot::RwLock via the parking_lot feature), ThreadLocal, and atomic operations. The Registry uses AtomicUsize for span reference counts with orderings that mirror std::sync::Arc (Relaxed on increment, Release/Acquire fence on decrement), which is correct. Thread-local Cell<usize> is used for the CLOSE_COUNT counter, which is sound since Cell is not Sync. The SpanMatch filter state uses AtomicBool with Release/Acquire ordering. Justifies uses-concurrency, concurrency-safe, concurrency-documented.

The EnvFilter directive parser is a hand-written state machine over char_indices, which guarantees that all string slices fall on character boundaries. All reachable state transitions either advance the state machine or return Err. No panic paths exist in the parser from untrusted input. The field-value parser handles bool, u64, i64, f64 literals and falls back to either a regex Pattern (default) or a MatchDebug string matcher. The MatchDebug comparison is implemented without allocation by streaming fmt::Debug output through a custom fmt::Write implementation. The test suite at src/filter/env/directive.rs:482-890 covers normal directives, invalid targets, uppercase/lowercase levels, numeric levels, span names with special characters, and invalid inputs. The parser correctly handles the documented directive syntax target[span{field=value}]=level and all its optional components. Justifies impl-parser, parser-impl-safe, parser-impl-tested, parser-impl-correct.

One low-severity quality finding (FINDING-1) was raised: when EnvFilter is used with field-value filters and non-literal values, regex matching is enabled by default, which presents a ReDoS surface if filter strings originate from untrusted inputs. The documentation flags this and offers Builder::with_regex(false) as mitigation, but the API default is on.

The crate contains no obfuscated code, no base64 blobs, no hardcoded network endpoints, and no telemetry, justifying is-benign.

Conclusion

The codebase has 25 unsafe occurrences, all confined to downcast_raw implementations and their callers. Each is documented and none dereferences a pointer beyond a type-matched reference cast. The directive parser is safe for untrusted input at the parsing layer, though the default of enabling regex field-value matching introduces a ReDoS vector when filters are constructed from untrusted strings. One low-severity finding was raised relating to this default. The fmt::format::escape module provides explicit ANSI terminal injection mitigation.

Findings(1)

FINDING-1 quality low

EnvFilter regex field-value matching is default-on; unsafe for untrusted inputs

The EnvFilter directive parser accepts field-value filters of the form [{field=value}]. When the env-filter feature is enabled and field-value filters use non-literal values (strings that are not bool, u64, i64, or f64), those values are compiled as matchers regular expressions by default.

The documentation at src/filter/env/mod.rs lines 82-87 explicitly calls this out: "When filters are constructed from potentially untrusted inputs, disabling regular expression matching is strongly recommended." No enforcement exists at the API level; callers that pass user-controlled strings to EnvFilter::new() or EnvFilter::from_str() without disabling regex obtain a ReDoS surface.

This is a quality issue in that the API's default is unsafe for the common case of taking filter strings from an untrusted environment (e.g., a web request). The severity is low because: (a) the affected call sites are typically restricted to administrator-controlled inputs (the RUST_LOG env variable), and (b) the documentation does flag this concern.

Relates to parser-impl-safe.

Annotations(5)

src/filter/env/builder.rs

src/filter/env/builder.rs, line 186-220

    ///
    /// [default directive]: Self::with_default_directive
    pub fn from_env_lossy(&self) -> EnvFilter {
        let var = env::var(self.env_var_name()).unwrap_or_default();
        self.parse_lossy(var)
    }

    /// Returns a new [`EnvFilter`] from the directives in the configured
    /// environment variable. If the environment variable is unset, no directive is added.
    ///
    /// An error is returned if the environment contains invalid directives.
    ///
    /// If the environment variable is empty, then the [default directive]
    /// is used instead.
    ///
    /// [default directive]: Self::with_default_directive
    pub fn from_env(&self) -> Result<EnvFilter, FromEnvError> {
        let var = env::var(self.env_var_name()).unwrap_or_default();
        self.parse(var).map_err(Into::into)
    }

    /// Returns a new [`EnvFilter`] from the directives in the configured
    /// environment variable, or an error if the environment variable is not set
    /// or contains invalid directives.
    ///
    /// If the environment variable is empty, then the [default directive]
    /// is used instead.
    ///
    /// [default directive]: Self::with_default_directive
    pub fn try_from_env(&self) -> Result<EnvFilter, FromEnvError> {
        let var = env::var(self.env_var_name())?;
        self.parse(var).map_err(Into::into)
    }

    // TODO(eliza): consider making this a public API?

The EnvFilter builder reads the RUST_LOG environment variable (or a caller-configured variable name) via std::env::var. The variable name to read is set by the caller; the crate documents RUST_LOG as the default. Only the configured variable name is read; the environment is not enumerated. Justifies uses-environment, environment-safe.

src/filter/env/directive.rs

src/filter/env/directive.rs, line 121-238

    pub(super) fn parse(from: &str, regex: bool) -> Result<Self, ParseError> {
        let mut cur = Self {
            level: LevelFilter::TRACE,
            target: None,
            in_span: None,
            fields: Vec::new(),
        };

        #[derive(Debug)]
        enum ParseState {
            Start,
            LevelOrTarget { start: usize },
            Span { span_start: usize },
            Field { field_start: usize },
            Fields,
            Target,
            Level { level_start: usize },
            Complete,
        }

        use ParseState::*;
        let mut state = Start;
        for (i, c) in from.trim().char_indices() {
            state = match (state, c) {
                (Start, '[') => Span { span_start: i + 1 },
                (Start, c) if !['-', ':', '_'].contains(&c) && !c.is_alphanumeric() => {
                    return Err(ParseError::new())
                }
                (Start, _) => LevelOrTarget { start: i },
                (LevelOrTarget { start }, '=') => {
                    cur.target = Some(from[start..i].to_owned());
                    Level { level_start: i + 1 }
                }
                (LevelOrTarget { start }, '[') => {
                    cur.target = Some(from[start..i].to_owned());
                    Span { span_start: i + 1 }
                }
                (LevelOrTarget { start }, ',') => {
                    let (level, target) = match &from[start..] {
                        "" => (LevelFilter::TRACE, None),
                        level_or_target => match LevelFilter::from_str(level_or_target) {
                            Ok(level) => (level, None),
                            Err(_) => (LevelFilter::TRACE, Some(level_or_target.to_owned())),
                        },
                    };

                    cur.level = level;
                    cur.target = target;
                    Complete
                }
                (state @ LevelOrTarget { .. }, _) => state,
                (Target, '=') => Level { level_start: i + 1 },
                (Span { span_start }, ']') => {
                    cur.in_span = Some(from[span_start..i].to_owned());
                    Target
                }
                (Span { span_start }, '{') => {
                    cur.in_span = match &from[span_start..i] {
                        "" => None,
                        _ => Some(from[span_start..i].to_owned()),
                    };
                    Field { field_start: i + 1 }
                }
                (state @ Span { .. }, _) => state,
                (Field { field_start }, '}') => {
                    cur.fields.push(match &from[field_start..i] {
                        "" => return Err(ParseError::new()),
                        field => field::Match::parse(field, regex)?,
                    });
                    Fields
                }
                (Field { field_start }, ',') => {
                    cur.fields.push(match &from[field_start..i] {
                        "" => return Err(ParseError::new()),
                        field => field::Match::parse(field, regex)?,
                    });
                    Field { field_start: i + 1 }
                }
                (state @ Field { .. }, _) => state,
                (Fields, ']') => Target,
                (Level { level_start }, ',') => {
                    cur.level = match &from[level_start..i] {
                        "" => LevelFilter::TRACE,
                        level => LevelFilter::from_str(level)?,
                    };
                    Complete
                }
                (state @ Level { .. }, _) => state,
                _ => return Err(ParseError::new()),
            };
        }

        match state {
            LevelOrTarget { start } => {
                let (level, target) = match &from[start..] {
                    "" => (LevelFilter::TRACE, None),
                    level_or_target => match LevelFilter::from_str(level_or_target) {
                        Ok(level) => (level, None),
                        // Setting the target without the level enables every level for that target
                        Err(_) => (LevelFilter::TRACE, Some(level_or_target.to_owned())),
                    },
                };

                cur.level = level;
                cur.target = target;
            }
            Level { level_start } => {
                cur.level = match &from[level_start..] {
                    "" => LevelFilter::TRACE,
                    level => LevelFilter::from_str(level)?,
                };
            }
            Target | Complete => {}
            _ => return Err(ParseError::new()),
        };

        Ok(cur)
    }

The directive parser (Directive::parse) is a hand-written state machine that iterates over characters using char_indices. All string slicing uses byte offsets from char_indices, which returns character boundary offsets, so slices are always valid UTF-8. The parser returns Err(ParseError::new()) for unrecognized state transitions and terminates early on malformed input. No panics are reachable from well-formed or malformed input: all slice operations use char_indices-produced offsets and the match exhausts all possible (state, char) transitions. Field value substrings are delegated to field::Match::parse. Justifies impl-parser, parser-impl-safe, parser-impl-tested. The test suite at lines 482-890 covers normal cases, invalid targets, empty levels, span names with special characters, and invalid span chars.

src/fmt/format/escape.rs

The EscapeGuard type sanitizes ANSI/C1 control characters from formatted output to prevent terminal injection. It escapes ESC (\x1b), BEL (\x07), BS (\x08), FF (\x0c), DEL (\x7f), and all C1 control codes (\x80-\x9f). This implementation is correct and complete for ANSI escape sequence injection prevention in terminal output.

src/layer/mod.rs

src/layer/mod.rs, line 1529-1558

    unsafe {
        // Safety: we're not actually *doing* anything with this pointer ---
        // this only care about the `Option`, which is essentially being used
        // as a bool. We can rely on the pointer being valid, because it is
        // a crate-private type, and is only returned by the `Layer` impl
        // for `Option`s. However, even if the layer *does* decide to be
        // evil and give us an invalid pointer here, that's fine, because we'll
        // never actually dereference it.
        layer.downcast_raw(TypeId::of::<NoneLayerMarker>())
    }
    .is_some()
}

/// Is a type implementing `Subscriber` `Option::<_>::None`?
pub(crate) fn subscriber_is_none<S>(subscriber: &S) -> bool
where
    S: Subscriber,
{
    unsafe {
        // Safety: we're not actually *doing* anything with this pointer ---
        // this only care about the `Option`, which is essentially being used
        // as a bool. We can rely on the pointer being valid, because it is
        // a crate-private type, and is only returned by the `Layer` impl
        // for `Option`s. However, even if the subscriber *does* decide to be
        // evil and give us an invalid pointer here, that's fine, because we'll
        // never actually dereference it.
        subscriber.downcast_raw(TypeId::of::<NoneLayerMarker>())
    }
    .is_some()
}

All 25 unsafe occurrences in the codebase fall into two categories:

  1. downcast_raw implementations (23 sites): These implement the Subscriber trait's unsafe method, which returns a raw pointer to self or an inner field when the requested TypeId matches. No pointer arithmetic or indexing is involved; the returned pointer is always a valid reference cast, and the safety comments note that the pointer is only used as a boolean signal (the NoneLayerMarker pattern) or is dereferenced only after a TypeId check that confirms the type. See src/layer/layered.rs:78-86, src/layer/mod.rs:1529-1538, src/filter/layer_filters/mod.rs:1309-1317.

  2. Two helper functions that call downcast_raw to probe for a marker type: layer_is_none and subscriber_is_none in src/layer/mod.rs, and layer_has_plf in src/filter/layer_filters/mod.rs. All three explicitly document that the returned pointer is never dereferenced.

No unsafe block performs raw memory allocation, pointer arithmetic, or FFI. All safety comments are present. Justifies uses-unsafe, unsafe-safe, unsafe-documented, unsafe-minimal.

src/registry/sharded.rs

The Registry struct wraps sharded_slab::Pool<DataInner> for concurrent span storage. Span IDs are derived from slab indices via idx_to_id / id_to_idx with a +1 bias to satisfy Id::from_u64's non-zero requirement. Reference counting is done with AtomicUsize with Relaxed ordering on fetch_add (mirroring Arc's clone semantics) and Release/Acquire on decrement/fence (mirroring Arc's drop semantics). The close-count mechanism uses a Cell<usize> in thread-local storage (CLOSE_COUNT), which is correct because cell values are inherently thread-local. No unsafe code is present in this file itself; the sharded slab provides the unsafe abstraction boundary. Justifies uses-concurrency, concurrency-safe.