rust-lang / rust-lang/rust

Safe custom `Pattern` can bind a valid `Searcher` to a different haystack, causing out-of-bounds reads

Open
#158,265 1 comment 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

A-str C-bug I-unsound requires-nightly T-libs
Dominant language
Rust
Stars
119k
Forks
16.1k
PR merge metrics
PR metrics pending

Description

Summary

Pattern is a safe trait, but its default consumers assume that the
Searcher returned by Pattern::into_searcher was constructed for the exact
haystack argument they supplied.

A safe custom Pattern implementation can ignore that argument and return a
standard-library Searcher constructed for a different, 'static string.
That searcher satisfies its own unsafe Searcher contract: all of its ranges
are valid for the string returned by Searcher::haystack(). However, consumers
such as Pattern::strip_prefix_of apply those ranges unchecked to the original
string instead. This can construct an invalid &str and lead to out-of-bounds
reads through safe code.

The reproducer contains no user-written unsafe but can make out of bounds reads.

Reproducer

#![feature(pattern)]
#![forbid(unsafe_code)]

use std::str::pattern::Pattern;

struct WrongHaystack;

impl Pattern for WrongHaystack {
    type Searcher<'a> = <&'static str as Pattern>::Searcher<'a>;

    fn into_searcher(self, _requested_haystack: &str) -> Self::Searcher<'_> {
        // This is a valid standard-library Searcher for OTHER. It is not a
        // searcher for the one-byte string passed to strip_prefix below.
        const OTHER: &str = "AAAAAAAA";
        <&'static str as Pattern>::into_searcher(OTHER, OTHER)
    }
}

fn main() {
    let original: Box<str> = "x".into();
    let suffix = original.strip_prefix(WrongHaystack).unwrap();

    println!("original length: {}", original.len());
    println!("suffix length:   {}", suffix.len());

    // The corrupted length makes safe indexing accept this access. black_box
    // ensures that the optimized native program performs the load.
    let first = std::hint::black_box(suffix.as_bytes())[0];
    println!("first suffix byte: {first}");
}

See also in the playground

On a 64-bit optimized build, the invalid suffix has a wrapped length:

$ rustc +nightly -O repro.rs && ./repro
original length: 1
suffix length:   18446744073709551609
first suffix byte: 0

The final value is of course not meaningful because the load is already
undefined behavior. AddressSanitizer confirms that it is physically outside
the allocation:

$ rustc +nightly -O -g -Zsanitizer=address repro.rs -o repro-asan
$ ./repro-asan
original length: 1
suffix length:   18446744073709551609
ERROR: AddressSanitizer: heap-buffer-overflow
READ of size 1
... is located 7 bytes after 1-byte region

and Miri also diagnoses:

unsafe precondition(s) violated: str::get_unchecked requires that the range is within the string slice

I used Codex to find this issue but manually verified it.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with the std::str::pattern::Pattern::strip_prefix default consumer and the Pattern::into_searcher and Searcher::haystack contracts described in the issue. Run the provided WrongHaystack reproducer under Miri or AddressSanitizer. Done means the reproducer can no longer create an invalid str or out-of-bounds read through safe code.

Written by the indexing model from the issue text.

Assessment

Tech stack
rust
Domain
security
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Quiet
Clarity
Needs clarification
Newbie friendliness
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.