The &str type is allocated on the stack directly due to its fixed size.
Like with String, the pointer and length -- the actual things that make up the type -- are stored on the stack, but in the case of &str, the pointed-to string data may live anywhere: if it came from a String, the data lives where the String owns it on the heap, while if it came from an array, it lives on the stack.
Roughly speaking, a &str is represented by
struct StrRef {
data: *const u8,
len: usize,
}
So this is constructed from a String by doing something like
let owned = String::from("hello");
let borrowed = StrRef {
data: owned.as_ptr(), // via Deref<Target = str>,
len: owned.len(),
};
More strictly speaking, str (without the &) is a dynamically sized type, which means that a reference to it (&str) is wide -- it stores both a pointer to the data and some metadata, which in the case of str and [T] is a usize representing the length.
This wide pointer ends up looking something like StrRef above, but the exact representation of wide pointers is (currently) not a stable part of the language; str and [T] are treated specially by the compiler, as is dyn Trait (where the metadata is a pointer to a vtable).