diff --git a/crates/ibt/src/raw.rs b/crates/ibt/src/raw.rs index 4ee8689..70f9770 100644 --- a/crates/ibt/src/raw.rs +++ b/crates/ibt/src/raw.rs @@ -104,8 +104,15 @@ impl Header { Ok(header) } + /// Dereference a raw pointer into a raw header + /// + /// # Safety + /// + /// The pointer must be non-null, aligned, and point to the start of a `Header`. pub unsafe fn from_raw_ptr(ptr: *const Self) -> Result { + // SAFETY: the pointer points to a `Header` let header = unsafe { *ptr }; + // sanity check, if this fails it could indicate undefined behavior, or simply that iRacing updated this API. if header.ver != 2 { return Err(RawTelemError::InvalidApiVersion(header.ver)); } diff --git a/crates/irsdk/src/client.rs b/crates/irsdk/src/client.rs index c6a0160..48c65e1 100644 --- a/crates/irsdk/src/client.rs +++ b/crates/irsdk/src/client.rs @@ -46,14 +46,17 @@ pub struct IRacingClient { impl IRacingClient { pub fn connect() -> Result { let mem_map = TelemetryMemMap::connect()?; - mem_map.wait_for_event_signal(TIMEOUT)?; + mem_map.wait_for_event_signal(TIMEOUT)?; + // SAFETY: we've waited on the signal let raw_header = unsafe { mem_map.as_raw_header()? }; let header = Header::from_raw(&raw_header)?; // Read the var headers once let vh_offset = raw_header.var_header_offset as usize; let vh_len = raw::VAR_HEADER_SIZE * raw_header.num_vars as usize; + // SAFETY: we've waited on the signal. offset and len come from the header. + // Data is copied immediately after. let vh_slice = unsafe { mem_map.as_slice(vh_offset, vh_len) }; let var_headers = raw::VarHeader::slice_from_fraw_bytes(vh_slice) @@ -69,10 +72,8 @@ impl IRacingClient { } fn next_raw_header(&self) -> Result { - // wait for event signal self.mem_map.wait_for_event_signal(TIMEOUT)?; - - // read the header + // SAFETY: we've waited on the signal let raw_header = unsafe { self.mem_map.as_raw_header() }?; if raw_header.status != 1 { @@ -99,6 +100,10 @@ impl IRacingClient { .process_results(|a| a.max_by_key(|vb| vb.tick_count))? .expect("there are always four var bufs"); + // SAFETY: + // - We waited on the signal in `self.next_raw_header()` + // - Offset and len come from the `VarBuf` in the header + // - We copy the data with `Sample::new_as_owned` let sample_slice = unsafe { self.mem_map .as_slice(newest_var_buf.buf_offset, self.buf_len) @@ -119,6 +124,10 @@ impl IRacingClient { .process_results(|a| a.max_by_key(|vb| vb.tick_count))? .expect("there are always four var bufs"); + // SAFETY: + // - We waited on the signal in `self.next_raw_header()` + // - Offset and len come from the `VarBuf` in the header + // - We copy the data into the given buffer before returning let sample_slice = unsafe { self.mem_map .as_slice(newest_var_buf.buf_offset, self.buf_len) diff --git a/crates/irsdk/src/win.rs b/crates/irsdk/src/win.rs index 561d0d3..23ff715 100644 --- a/crates/irsdk/src/win.rs +++ b/crates/irsdk/src/win.rs @@ -20,10 +20,15 @@ pub struct WindowsError(#[from] windows::core::Error); impl WindowsError { fn from_last_error() -> Self { + // SAFETY: ffi, always safe to call let code = unsafe { GetLastError().to_hresult() }; let err = windows::core::Error::from_hresult(code); err.into() } + + pub fn is_file_not_found(&self) -> bool { + self.0.code().0 == FILE_NOT_FOUND_CODE + } } #[derive(Clone, Debug, thiserror::Error)] @@ -34,12 +39,6 @@ pub enum SignalError { Windows(WindowsError), } -impl WindowsError { - pub fn is_file_not_found(&self) -> bool { - self.0.code().0 == FILE_NOT_FOUND_CODE - } -} - #[derive(Debug)] pub struct TelemetryMemMap { file_mapping_handle: HANDLE, @@ -49,6 +48,7 @@ pub struct TelemetryMemMap { impl TelemetryMemMap { pub fn connect() -> Result { + // SAFETY: ffi let file_mapping_handle = unsafe { OpenFileMappingW(FILE_MAP_READ.0, false, MEM_MAP_FILE_NAME)? }; let mem_map_address = unsafe { MapViewOfFile(file_mapping_handle, FILE_MAP_READ, 0, 0, 0) }; @@ -62,7 +62,9 @@ impl TelemetryMemMap { }) } + /// Block the thread until iRacing signals it has finished writing data pub fn wait_for_event_signal(&self, timeout: Duration) -> Result<(), SignalError> { + // SAFETY: the handle was successfully obtained from `OpenEventW` let result = unsafe { WaitForSingleObject(self.event_handle, timeout.as_millis() as u32) }; // see https://learn.microsoft.com/en-us/windows/win32/api/synchapi/nf-synchapi-waitforsingleobject#return-value match result.0 { @@ -73,14 +75,37 @@ impl TelemetryMemMap { } } + /// Interpret the start of the memory-mapped file as a [`raw::Header`] + /// + /// The data is copied. + /// + /// # Safety + /// + /// Callers must have called [`TelemetryMemMap::wait_for_event_signal`] before this to provide + /// assurance that nothing is writing to this region of memory while we read it. pub unsafe fn as_raw_header(&self) -> Result { let ptr = self.mem_map_address.Value as *const raw::Header; + // SAFETY: the start of the memory-mapped file is always a valid `raw::Header` unsafe { raw::Header::from_raw_ptr(ptr) } } + /// Interpret a region of the memory-mapped file as a slice of raw bytes. + /// + /// The data is *not* copied. + /// + /// # Safety + /// + /// - Callers must have called [`TelemetryMemMap::wait_for_event_signal`] before this to provide + /// assurance that nothing is writing to this region of memory while we read it. + /// - A slice constructed from the given offset + len must lie entirely within the memory-mapped file. + /// - The data must be promptly copied to ensure it is not mutated within the lifetime of the returned slice. pub unsafe fn as_slice(&self, offset: usize, len: usize) -> &[u8] { unsafe { let ptr = (self.mem_map_address.Value as *const u8).add(offset); + // SAFETY: Assuming the caller upheld the invariants, then the `from_raw_parts` invariants are also upheld: + // - offest + len lies within the same memory-mapped file + // - we are only pointing to bytes + // - callers guarantee copying of data std::slice::from_raw_parts(ptr, len) } }