Safe custom `Pattern` can bind a valid `Searcher` to a different haystack, causing out-of-bounds reads
Nobody has claimed this yet.
- 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
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- 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