Skip to content

Soundness Bug in this crate #166

@lewismosciski

Description

@lewismosciski

Hi there!

We scanned the most popular libraries on crates.io and found some memory safety bugs in this library.

PoC

use std::fmt;

fn main() {
    // Contains an escape char so we also exercise the escaping path,
    // and invalid UTF-8 bytes to poison the unchecked &str slices.
    let bytes: &[u8] = &[0xFF, b'&', 0xFF];

    struct Trigger<'a>(&'a [u8]);

    impl fmt::Display for Trigger<'_> {
        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
            // This is the crate's safe API that internally calls from_utf8_unchecked.
            v_htmlescape::scalar::_escape(self.0, f)
        }
    }

    // Get a String back from formatting.
    let out = format!("{}", Trigger(bytes));

    // Force UTF-8-dependent processing (should trip Miri if invalid UTF-8 got into a &str).
    // Note: If the formatter happened to copy raw bytes, `out` could still be valid UTF-8;
    // but if invalid `&str` was used, this will reliably diagnose.
    let mut sum = 0usize;
    for ch in out.chars() {
        sum = sum.wrapping_add(ch as usize);
    }
    std::hint::black_box(sum);
}

Miri Output

error: Undefined Behavior: constructing invalid value: encountered 0x001e686d, but expected a valid unicode scalar value (in `0..=0x10FFFF` but not in `0xD800..=0xDFFF`)
   --> /home/ccuu/.rustup/toolchains/nightly-2025-10-09-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/char/methods.rs:239:18
    |
239 |         unsafe { super::convert::from_u32_unchecked(i) }
    |                  ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Undefined Behavior occurred here
    |
    = help: this indicates a bug in the program: it performed an invalid operation, and caused Undefined Behavior
    = help: see https://doc.rust-lang.org/nightly/reference/behavior-considered-undefined.html for further information
    = note: BACKTRACE:
    = note: inside `std::char::methods::<impl char>::from_u32_unchecked` at /home/ccuu/.rustup/toolchains/nightly-2025-10-09-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/char/methods.rs:239:18: 239:55
    = note: inside closure at /home/ccuu/.rustup/toolchains/nightly-2025-10-09-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/str/iter.rs:42:59: 42:87
    = note: inside `std::option::Option::<u32>::map::<char, {closure@<std::str::Chars<'_> as std::iter::Iterator>::next::{closure#0}}>` at /home/ccuu/.rustup/toolchains/nightly-2025-10-09-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/option.rs:1159:29: 1159:33
    = note: inside `<std::str::Chars<'_> as std::iter::Iterator>::next` at /home/ccuu/.rustup/toolchains/nightly-2025-10-09-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/str/iter.rs:42:18: 42:88
note: inside `main`
   --> src/main.rs:28:15
    |
 28 |     for ch in out.chars() {
    |               ^^^^^^^^^^^

note: some details are omitted, run with `MIRIFLAGS=-Zmiri-backtrace=full` for a verbose backtrace

error: aborting due to 1 previous error

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions