cargo / toml_edit / audit
cargo : toml_edit @ 0.25.11+spec-1.1.0
PE Patrick Elsen signed 2026-05-27 published 2026-05-27

Claims

has-binarieshas-build-exechas-fuzz-testshas-install-exechas-integration-testshas-property-testshas-unit-testsimpl-algorithmimpl-concurrencyimpl-cryptoimpl-datastructureimpl-interpreterimpl-jitimpl-parserimpl-protocolis-benignparser-impl-safeparser-impl-testeduses-concurrencyuses-cryptouses-environmentuses-execuses-filesystemuses-interpreteruses-jituses-networkuses-unsafe

Summary

toml_edit 0.25.11 is a format-preserving TOML parser and editor used by Cargo; it contains zero unsafe blocks, enforces a recursion limit of 80 levels (overridable via an opt-in unbounded feature), and is exercised by a TOML compliance test suite and proptest round-trip properties. No findings were produced.

Report

Subject

toml_edit 0.25.11 is a format-preserving TOML parser and editor. It parses a TOML document into a rich, mutable tree that records the raw text of every token (keys, values, whitespace, comments) so that round-trip read-modify-write leaves unmodified parts of the file byte-identical. This places it in Cargo's dependency graph: Cargo itself uses toml_edit to read and modify Cargo.toml files. The public API exposes Document<S> (immutable, referencing the original &str) and DocumentMut (owned, editable), along with Serde de/ser support gated behind the serde feature.

Methodology

All source files in contents/src/ were read in full (9933 LOC across 28 files). The following tools were used: openvet 0.6.0, grep, diff, wc. Standard source surveys were run for unsafe blocks, FFI, network, filesystem, process, environment, crypto, RNG, and concurrency; all returned empty. The diff between contents/ and vcs/ showed only expected differences: cargo-normalised Cargo.toml, the absent CHANGELOG.md, a Cargo.lock present in contents, and a tests/ tree present only in VCS. No source file differed between the two trees; no binary files appeared in contents/ that were absent from vcs/. The TOML spec referenced is TOML v1.1.0, per the version suffix +spec-1.1.0.

Results

The diff between contents/ and vcs/ confirms byte-equivalent source code. The only divergences are the cargo-normalised Cargo.toml, a Cargo.lock bundled in the published crate, the excluded CHANGELOG.md, and the excluded tests/ directory (integration tests live in VCS but not in the crate archive per the include manifest field). is-benign: no obfuscated code, no base64 blobs, no telemetry, no suspicious network endpoints or timing-sensitive behaviour was found.

has-binaries=false: no pre-compiled binary assets. has-build-exec=false: no build.rs, no proc-macro. has-install-exec=false.

has-unit-tests=true: 15 #[test] functions are inline in src/ (in error.rs, key.rs, item.rs, value.rs, document.rs, encode.rs). has-integration-tests=true: five integration test binaries are declared in Cargo.toml.orig (testsuite, compliance, decoder_compliance, encoder_compliance, serde); the test source lives in vcs/tests/. has-property-tests=true: two proptest! blocks in src/encode.rs exercise parseable_string and parseable_key round-trip invariants over arbitrary Unicode strings. has-fuzz-tests=false: no fuzz corpus or harness.

uses-unsafe=false: grep over contents/src/ found zero unsafe keywords. The crate delegates all unsafe code to its dependencies (indexmap, toml_parser, winnow). uses-network=false, uses-filesystem=false, uses-environment=false, uses-exec=false, uses-jit=false, uses-interpreter=false, uses-concurrency=false, uses-crypto=false: all confirmed empty by source survey. impl-crypto=false, impl-interpreter=false, impl-jit=false, impl-protocol=false, impl-datastructure=false, impl-algorithm=false, impl-concurrency=false: the crate implements none of these; it implements only a TOML parser and document editor.

impl-parser=true: the parse feature wires a two-stage pipeline. Stage one calls toml_parser::parser::parse_document (audited separately) to produce a token/event stream. Stage two, in src/parser/, traverses that event stream with winnow's TokenSlice and constructs the toml_edit document tree. The recursion guard wraps the event sink with RecursionGuard::new(&mut receiver, LIMIT) where LIMIT = 80 (defined in src/parser/mod.rs:142). The unbounded feature disables this guard; its documentation explicitly warns that callers become responsible for stack overflow. Dotted-key path depth is additionally bounded in src/parser/key.rs:67-70 with its own check against LIMIT. parser-impl-safe=true: the event-driven parser in src/parser/ contains no unsafe blocks, performs no unbounded recursion itself (the recursive descend_path calls are bounded by the key path depth limit and by RecursionGuard), and all index arithmetic goes through safe Rust slice indexing. Error paths call errors.report_error(...) rather than panicking. The only panics present are in RawString::to_str and to_str_with_default, which fire only when a span references a position outside the source string — a condition that cannot be triggered by adversarial input because spans are computed directly from the parser's own event stream over the same source. parser-impl-tested=true: the two proptest blocks cover string and key encoding round-trips; the integration tests include compliance suites against the TOML test data corpus (toml-test-data, toml-test-harness), exercising decoder and encoder conformance.

Conclusion

toml_edit 0.25.11 implements a format-preserving TOML editor over a two-stage parser pipeline. The crate contains zero unsafe blocks; all low-level operations delegate to audited dependencies. The recursion limit of 80 nesting levels is enforced by RecursionGuard from toml_parser and by an explicit key-path depth check in the event consumer, except under the unbounded feature which is opt-in and carries explicit stack-overflow warnings in the Cargo.toml comment. The property-test suite covers encoding round-trips; integration tests run against the TOML compliance corpus. No findings were produced.

Findings

No findings.

Annotations(4)

src/encode.rs

src/encode.rs, line 407-449

    proptest! {
        #[test]
        #[cfg(feature = "parse")]
        fn parseable_string(string in "\\PC*") {
            let value = Value::from(string.clone());
            let encoded = value.to_string();
            let _: Value = encoded.parse().unwrap_or_else(|err| {
                panic!("error: {err}

string:
```
{string}
```
value:
```
{value}
```
")
            });
        }
    }

    proptest! {
        #[test]
        #[cfg(feature = "parse")]
        fn parseable_key(string in "\\PC*") {
            let key = Key::new(string.clone());
            let encoded = key.to_string();
            let _: Key = encoded.parse().unwrap_or_else(|err| {
                panic!("error: {err}

string:
```
{string}
```
key:
```
{key}
```
")
            });
        }
    }

Two proptest! blocks exercise encoding round-trips: parseable_string generates arbitrary Unicode strings, converts them to Value::String, encodes them, and parses the result; parseable_key does the same for Key. These property tests verify that any string can survive an encode-then-parse cycle.

Justifies has-property-tests=true, parser-impl-tested=true.

src/parser/key.rs

src/parser/key.rs, line 66-72

    #[cfg(not(feature = "unbounded"))]
    if super::LIMIT <= result_path.len() as u32 {
        errors.report_error(ParseError::new("recursion limit"));
        return (Vec::new(), None);
    }

    (result_path, result_key)

After the key path is assembled, the consumer checks super::LIMIT <= result_path.len() as u32 and calls errors.report_error(ParseError::new("recursion limit")) if the dotted key nesting depth exceeds the limit. This is a second, independent depth guard complementing the RecursionGuard in the parser entry point.

Justifies parser-impl-safe=true.

src/parser/mod.rs

src/parser/mod.rs, line 17-142

pub(crate) fn parse_document<'s>(
    source: toml_parser::Source<'s>,
    errors: &mut dyn prelude::ErrorSink,
) -> crate::Document<&'s str> {
    let tokens = source.lex().into_vec();

    let mut events = Vec::with_capacity(tokens.len());
    let mut receiver = ValidateWhitespace::new(&mut events, source);
    #[cfg(not(feature = "unbounded"))]
    let mut receiver = RecursionGuard::new(&mut receiver, LIMIT);
    #[cfg(not(feature = "unbounded"))]
    let receiver = &mut receiver;
    #[cfg(feature = "unbounded")]
    let receiver = &mut receiver;
    toml_parser::parser::parse_document(&tokens, receiver, errors);

    let mut input = prelude::Input::new(&events);
    let doc = document::document(&mut input, source, errors);
    doc
}

pub(crate) fn parse_key(
    source: toml_parser::Source<'_>,
    errors: &mut dyn prelude::ErrorSink,
) -> crate::Key {
    let tokens = source.lex().into_vec();

    let mut events = Vec::with_capacity(tokens.len());
    let mut receiver = ValidateWhitespace::new(&mut events, source);
    #[cfg(not(feature = "unbounded"))]
    let mut receiver = RecursionGuard::new(&mut receiver, LIMIT);
    #[cfg(not(feature = "unbounded"))]
    let receiver = &mut receiver;
    #[cfg(feature = "unbounded")]
    let receiver = &mut receiver;
    toml_parser::parser::parse_simple_key(&tokens, receiver, errors);

    if let Some(event) = events
        .iter()
        .find(|e| e.kind() == toml_parser::parser::EventKind::SimpleKey)
    {
        let (raw, key) = key::on_simple_key(event, source, errors);
        crate::Key::new(key).with_repr_unchecked(crate::Repr::new_unchecked(raw))
    } else {
        let key = source.input();
        let raw = RawString::with_span(0..source.input().len());
        crate::Key::new(key).with_repr_unchecked(crate::Repr::new_unchecked(raw))
    }
}

pub(crate) fn parse_key_path(
    source: toml_parser::Source<'_>,
    errors: &mut dyn prelude::ErrorSink,
) -> Vec<crate::Key> {
    let tokens = source.lex().into_vec();

    let mut events = Vec::with_capacity(tokens.len());
    let mut receiver = ValidateWhitespace::new(&mut events, source);
    #[cfg(not(feature = "unbounded"))]
    let mut receiver = RecursionGuard::new(&mut receiver, LIMIT);
    #[cfg(not(feature = "unbounded"))]
    let receiver = &mut receiver;
    #[cfg(feature = "unbounded")]
    let receiver = &mut receiver;
    toml_parser::parser::parse_key(&tokens, receiver, errors);

    let mut input = prelude::Input::new(&events);
    let mut prefix = None;
    let mut path = None;
    let mut key = None;
    let mut suffix = None;
    while let Some(event) = input.next_token() {
        match event.kind() {
            toml_parser::parser::EventKind::Whitespace => {
                let raw = RawString::with_span(event.span().start()..event.span().end());
                if prefix.is_none() {
                    prefix = Some(raw);
                } else if suffix.is_none() {
                    suffix = Some(raw);
                }
            }
            _ => {
                let (local_path, local_key) = key::on_key(event, &mut input, source, errors);
                path = Some(local_path);
                key = local_key;
            }
        }
    }
    if let Some(mut key) = key {
        if let Some(prefix) = prefix {
            key.leaf_decor.set_prefix(prefix);
        }
        if let Some(suffix) = suffix {
            key.leaf_decor.set_suffix(suffix);
        }
        let mut path = path.unwrap_or_default();
        path.push(key);
        path
    } else {
        Default::default()
    }
}

pub(crate) fn parse_value(
    source: toml_parser::Source<'_>,
    errors: &mut dyn prelude::ErrorSink,
) -> crate::Value {
    let tokens = source.lex().into_vec();

    let mut events = Vec::with_capacity(tokens.len());
    let mut receiver = ValidateWhitespace::new(&mut events, source);
    #[cfg(not(feature = "unbounded"))]
    let mut receiver = RecursionGuard::new(&mut receiver, LIMIT);
    #[cfg(not(feature = "unbounded"))]
    let receiver = &mut receiver;
    #[cfg(feature = "unbounded")]
    let receiver = &mut receiver;
    toml_parser::parser::parse_value(&tokens, receiver, errors);

    let mut input = prelude::Input::new(&events);
    let value = value::value(&mut input, source, errors);
    value
}

#[cfg(not(feature = "unbounded"))]
const LIMIT: u32 = 80;

The parser entry points (parse_document, parse_key, parse_key_path, parse_value) all wrap the toml_parser event sink with RecursionGuard::new(&mut receiver, LIMIT) where LIMIT = 80. This is the primary depth-limit mechanism protecting against stack exhaustion on deeply nested TOML input. The unbounded feature disables this guard; the Cargo.toml.orig comment for that feature explicitly documents the stack-overflow risk.

Justifies impl-parser=true, parser-impl-safe=true.

src/raw_string.rs

src/raw_string.rs, line 39-83

    pub(crate) fn to_str<'s>(&'s self, input: &'s str) -> &'s str {
        match &self.0 {
            RawStringInner::Empty => "",
            RawStringInner::Explicit(s) => s.as_str(),
            RawStringInner::Spanned(span) => input
                .get(span.clone())
                .unwrap_or_else(|| panic!("span {span:?} should be in input:\n```\n{input}\n```")),
        }
    }

    pub(crate) fn to_str_with_default<'s>(
        &'s self,
        input: Option<&'s str>,
        default: &'s str,
    ) -> &'s str {
        match &self.0 {
            RawStringInner::Empty => "",
            RawStringInner::Explicit(s) => s.as_str(),
            RawStringInner::Spanned(span) => {
                if let Some(input) = input {
                    input.get(span.clone()).unwrap_or_else(|| {
                        panic!("span {span:?} should be in input:\n```\n{input}\n```")
                    })
                } else {
                    default
                }
            }
        }
    }

    pub(crate) fn despan(&mut self, input: &str) {
        match &self.0 {
            RawStringInner::Empty => {}
            RawStringInner::Explicit(_) => {}
            RawStringInner::Spanned(span) => {
                if span.start == span.end {
                    *self = Self(RawStringInner::Empty);
                } else {
                    *self = Self::from(input.get(span.clone()).unwrap_or_else(|| {
                        panic!("span {span:?} should be in input:\n```\n{input}\n```")
                    }));
                }
            }
        }
    }

to_str and to_str_with_default panic if a Spanned range is out-of-bounds for the provided input string. These panics are only reachable when spans were computed from the same source string, which is guaranteed by the parser (spans come from toml_parser::Span offsets into the original source). Adversarial input cannot produce an out-of-range span through normal API use because the span values are computed by the library, not provided by the caller.

Justifies parser-impl-safe=true.