Yes, this is sound. (There are also some crates like safe-transmute and dataview that provide safe methods for converting from &mut [i32] to &mut [u8], if you prefer to depend on someone else's unsafe code.)
You can avoid allocating an intermediate u8 buffer by using a [u8; 4] array on the stack, and i32::from_ne_bytes() avoids the implicit transmute you get from type punning slice.
As long as R does some sort of buffering under the hood, I would expect something like this to have identical performance while also not requiring unsafe code.
pub fn read_buffer_i32_safe<R: Read>(reader: &mut R, n: usize) -> Result<Vec<i32>> {
let mut numbers = Vec::with_capacity(n);
for _ in 0..n {
let mut buffer = [0; std::mem::size_of::<i32>()];
reader.read_exact(&mut buffer)?;
numbers.push(i32::from_ne_bytes(buffer));
}
Ok(numbers)
}