Skip to main content

native_ipc_core/
mapping.rs

1//! Audited conversion from native mapping witnesses to atomic capabilities.
2
3use alloc::vec::Vec;
4use core::fmt;
5use core::mem::{align_of, size_of};
6use core::ptr::NonNull;
7
8use crate::layout::{LayoutError, RegionSetLayout, RoleId, ValidatedRegionLayout};
9use crate::slot::{
10    AcknowledgementCell, AcknowledgementObservation, AcknowledgementReader, AcknowledgementWriter,
11    ReaderSlot, SlotError, SlotMetadata, WriterSlot,
12};
13
14/// Native read-only mapping witness consumed by the audited binding boundary.
15///
16/// # Safety
17///
18/// The base and length must describe one live, initialized allocation for the
19/// witness lifetime. It must be OS-enforced read-only locally, cover exactly
20/// the bytes used to create the supplied `ValidatedRegionLayout`, and remain
21/// mapped while a bound region owns the witness.
22pub unsafe trait ReadOnlyMapping {
23    /// Mapping base with allocation provenance.
24    fn base(&self) -> NonNull<u8>;
25    /// Exact native capability size.
26    fn len(&self) -> usize;
27    /// Returns whether the capability is empty (valid witnesses are not).
28    fn is_empty(&self) -> bool {
29        self.len() == 0
30    }
31}
32
33/// Native sole-writer mapping witness consumed by the audited binding boundary.
34///
35/// # Safety
36///
37/// In addition to the allocation requirements of `ReadOnlyMapping`, this must
38/// be the only writable mapping for the region and safe code must be unable to
39/// duplicate the witness or recover a second writer while it is owned here.
40pub unsafe trait SoleWriterMapping {
41    /// Mapping base with allocation provenance.
42    fn base(&self) -> NonNull<u8>;
43    /// Exact native capability size.
44    fn len(&self) -> usize;
45    /// Returns whether the capability is empty (valid witnesses are not).
46    fn is_empty(&self) -> bool {
47        self.len() == 0
48    }
49}
50
51/// Failure to bind a validated layout to its native mapping witness.
52#[derive(Clone, Copy, Debug, Eq, PartialEq)]
53pub enum BindingError {
54    /// Native capability size differs from the validated range.
55    MappingSizeMismatch {
56        /// Size validated while the mapping was quiescent.
57        expected: usize,
58        /// Size reported by the platform witness.
59        actual: usize,
60    },
61    /// Checked record address is not aligned for its atomic representation.
62    MisalignedRecord,
63    /// Validated layout rejected the selected route or index.
64    Layout(LayoutError),
65    /// Shared metadata no longer matches its validated generation.
66    Slot(SlotError),
67    /// Owned payload snapshot allocation failed.
68    AllocationFailed,
69    /// Caller payload length cannot be represented by the fixed protocol field.
70    PayloadLengthOverflow,
71    /// Caller destination is shorter than the observed payload.
72    DestinationTooSmall {
73        /// Bytes the observed payload occupies.
74        required: usize,
75        /// Bytes the caller supplied.
76        provided: usize,
77    },
78    /// Validated mapping does not belong to the supplied composed topology.
79    TopologyMismatch,
80    /// Composed topology has no exact route for the requested target slot.
81    MissingRoute {
82        /// Producer role requested from the bound topology.
83        target: RoleId,
84        /// Producer slot requested from the bound topology.
85        slot: u32,
86    },
87}
88
89impl fmt::Display for BindingError {
90    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
91        write!(formatter, "mapping binding failed: {self:?}")
92    }
93}
94
95#[cfg(feature = "std")]
96impl std::error::Error for BindingError {}
97
98impl From<LayoutError> for BindingError {
99    fn from(value: LayoutError) -> Self {
100        Self::Layout(value)
101    }
102}
103
104impl From<SlotError> for BindingError {
105    fn from(value: SlotError) -> Self {
106        Self::Slot(value)
107    }
108}
109
110/// Copies `len` externally mutable bytes without forming shared references.
111///
112/// # Safety
113///
114/// `source..source+len` must stay mapped and readable for the call; the
115/// destination must be valid for `len` disjoint writes.
116unsafe fn volatile_copy(source: *const u8, destination: *mut u8, len: usize) {
117    for index in 0..len {
118        // SAFETY: caller supplies bounds, liveness, and disjointness.
119        unsafe {
120            destination
121                .add(index)
122                .write(core::ptr::read_volatile(source.add(index)));
123        }
124    }
125}
126
127/// Mapping-lifetime owner that can mint acquire-only capabilities.
128pub struct ReaderRegion<M> {
129    mapping: M,
130    layout: ValidatedRegionLayout,
131    topology: RegionSetLayout,
132}
133
134impl<M: ReadOnlyMapping> ReaderRegion<M> {
135    /// Consumes a platform-minted read-only witness.
136    ///
137    /// On rejection, returns the witness alongside the error so the caller
138    /// recovers the mapping instead of losing it.
139    pub fn new(
140        mapping: M,
141        layout: ValidatedRegionLayout,
142        topology: RegionSetLayout,
143    ) -> Result<Self, (M, BindingError)> {
144        if let Err(error) = validate_mapping_size(mapping.len(), &layout) {
145            return Err((mapping, error));
146        }
147        if let Err(error) = validate_topology(&layout, &topology) {
148            return Err((mapping, error));
149        }
150        Ok(Self {
151            mapping,
152            layout,
153            topology,
154        })
155    }
156
157    /// Binds one checked slot without exposing shared bytes.
158    pub fn slot(&self, slot: u32) -> Result<ReaderSlot<'_>, BindingError> {
159        let binding = self.layout.reader_slot_binding(slot)?;
160        let range = self.layout.slot_range(slot)?;
161        let header = record::<SlotMetadata, _>(
162            &self.mapping,
163            self.mapping.base(),
164            range.start,
165            range.len(),
166        )?;
167        // SAFETY: the witness contract supplies provenance, lifetime, exact
168        // validation, and local read-only permission; `record` checked bounds/alignment.
169        Ok(unsafe { ReaderSlot::bind(header, binding) }?)
170    }
171
172    /// Copies one bounded hostile payload and rechecks its publication metadata.
173    ///
174    /// Same-sequence malicious mutation can still produce torn bytes; the
175    /// returned owned buffer must be decoded as hostile input.
176    pub fn copy_payload(&self, slot: u32, expected_sequence: u64) -> Result<Vec<u8>, BindingError> {
177        let observation = self.slot(slot)?.observe(expected_sequence)?;
178        let range = self
179            .layout
180            .slot_payload_range(slot, observation.payload_len())?;
181        let mut owned = Vec::<u8>::new();
182        owned
183            .try_reserve_exact(range.len())
184            .map_err(|_| BindingError::AllocationFailed)?;
185        // SAFETY: the read-only witness keeps this validated range mapped and
186        // readable. Each volatile byte load crosses the externally mutable
187        // memory boundary without forming a shared Rust reference, and each
188        // destination byte is initialized exactly once in disjoint owned memory.
189        unsafe {
190            owned.set_len(range.len());
191            volatile_copy(
192                self.mapping.base().as_ptr().add(range.start),
193                owned.as_mut_ptr(),
194                range.len(),
195            );
196        }
197        self.slot(slot)?.recheck(observation)?;
198        Ok(owned)
199    }
200
201    /// Copies one bounded hostile payload into caller storage and rechecks
202    /// its publication metadata, performing no allocation.
203    ///
204    /// Returns the copied payload length. Same-sequence malicious mutation
205    /// can still produce torn bytes; the destination prefix must be decoded
206    /// as hostile input. Bytes past the returned length are untouched.
207    pub fn copy_payload_into(
208        &self,
209        slot: u32,
210        expected_sequence: u64,
211        destination: &mut [u8],
212    ) -> Result<usize, BindingError> {
213        let observation = self.slot(slot)?.observe(expected_sequence)?;
214        let range = self
215            .layout
216            .slot_payload_range(slot, observation.payload_len())?;
217        if destination.len() < range.len() {
218            return Err(BindingError::DestinationTooSmall {
219                required: range.len(),
220                provided: destination.len(),
221            });
222        }
223        // SAFETY: the read-only witness keeps this validated range mapped and
224        // readable; the destination is caller-owned disjoint memory.
225        unsafe {
226            volatile_copy(
227                self.mapping.base().as_ptr().add(range.start),
228                destination.as_mut_ptr(),
229                range.len(),
230            );
231        }
232        self.slot(slot)?.recheck(observation)?;
233        Ok(range.len())
234    }
235
236    /// Releases the binding and returns the owned mapping witness.
237    pub fn into_mapping(self) -> M {
238        self.mapping
239    }
240
241    /// Binds one checked acknowledgement route without exposing shared bytes.
242    pub fn acknowledgement(
243        &self,
244        target: RoleId,
245        slot: u32,
246    ) -> Result<AcknowledgementReader<'_>, BindingError> {
247        let route = self
248            .topology
249            .acknowledgement_route(target, slot)
250            .ok_or(BindingError::MissingRoute { target, slot })?;
251        let binding = self.layout.acknowledgement_reader_binding(route)?;
252        let range = self.layout.acknowledgement_range(route.cell_index())?;
253        let cell = record::<AcknowledgementCell, _>(
254            &self.mapping,
255            self.mapping.base(),
256            range.start,
257            range.len(),
258        )?;
259        // SAFETY: same witness and checked-record proof as `slot`.
260        Ok(unsafe { AcknowledgementReader::bind(cell, binding) })
261    }
262}
263
264/// Mapping-lifetime owner that prevents duplicate safe writer binding.
265pub struct WriterRegion<M> {
266    mapping: M,
267    layout: ValidatedRegionLayout,
268    topology: RegionSetLayout,
269}
270
271impl<M: SoleWriterMapping> WriterRegion<M> {
272    /// Consumes a platform-minted unique writer witness.
273    ///
274    /// On rejection, returns the witness alongside the error so the caller
275    /// recovers the mapping instead of losing it.
276    pub fn new(
277        mapping: M,
278        layout: ValidatedRegionLayout,
279        topology: RegionSetLayout,
280    ) -> Result<Self, (M, BindingError)> {
281        if let Err(error) = validate_mapping_size(mapping.len(), &layout) {
282            return Err((mapping, error));
283        }
284        if let Err(error) = validate_topology(&layout, &topology) {
285            return Err((mapping, error));
286        }
287        Ok(Self {
288            mapping,
289            layout,
290            topology,
291        })
292    }
293
294    /// Releases the binding and returns the owned mapping witness.
295    pub fn into_mapping(self) -> M {
296        self.mapping
297    }
298
299    /// Exclusively binds one checked producer slot.
300    pub fn slot(&mut self, slot: u32) -> Result<WriterSlot<'_>, BindingError> {
301        let target = self.layout.role();
302        let route = self
303            .topology
304            .acknowledgement_route(target, slot)
305            .ok_or(BindingError::MissingRoute { target, slot })?;
306        let binding = self.layout.writer_slot_binding(route)?;
307        let range = self.layout.slot_range(route.slot_index())?;
308        let header = record::<SlotMetadata, _>(
309            &self.mapping,
310            self.mapping.base(),
311            range.start,
312            range.len(),
313        )?;
314        // SAFETY: consuming the unique witness and borrowing `self` mutably
315        // prevent a second safe writer capability while this borrow is live.
316        Ok(unsafe { WriterSlot::bind(header, binding) }?)
317    }
318
319    /// Copies an owned caller payload into a checked slot and publishes it.
320    pub fn publish(
321        &mut self,
322        slot: u32,
323        sequence: u64,
324        acknowledgement: Option<AcknowledgementObservation>,
325        payload: &[u8],
326    ) -> Result<(), BindingError> {
327        let payload_len =
328            u32::try_from(payload.len()).map_err(|_| BindingError::PayloadLengthOverflow)?;
329        let range = self.layout.slot_payload_range(slot, payload_len)?;
330        let base = self.mapping.base();
331        let mut bound_slot = self.slot(slot)?;
332        let reservation = bound_slot.prepare_publish(sequence, acknowledgement)?;
333        // SAFETY: the unique writer witness and `&mut self` exclude other
334        // writers; the checked payload range is disjoint from slot metadata.
335        unsafe {
336            core::ptr::copy_nonoverlapping(
337                payload.as_ptr(),
338                base.as_ptr().add(range.start),
339                payload.len(),
340            );
341        }
342        reservation.publish(payload_len)?;
343        Ok(())
344    }
345
346    /// Exclusively binds one checked acknowledgement cell.
347    pub fn acknowledgement(
348        &mut self,
349        target: RoleId,
350        slot: u32,
351    ) -> Result<AcknowledgementWriter<'_>, BindingError> {
352        let route = self
353            .topology
354            .acknowledgement_route(target, slot)
355            .ok_or(BindingError::MissingRoute { target, slot })?;
356        let binding = self.layout.acknowledgement_writer_binding(route)?;
357        let range = self.layout.acknowledgement_range(route.cell_index())?;
358        let cell = record::<AcknowledgementCell, _>(
359            &self.mapping,
360            self.mapping.base(),
361            range.start,
362            range.len(),
363        )?;
364        // SAFETY: same unique witness and exclusive-borrow proof as `slot`.
365        Ok(unsafe { AcknowledgementWriter::bind(cell, binding) })
366    }
367}
368
369fn validate_mapping_size(
370    actual: usize,
371    layout: &ValidatedRegionLayout,
372) -> Result<(), BindingError> {
373    if actual == layout.mapping_size() {
374        Ok(())
375    } else {
376        Err(BindingError::MappingSizeMismatch {
377            expected: layout.mapping_size(),
378            actual,
379        })
380    }
381}
382
383fn validate_topology(
384    layout: &ValidatedRegionLayout,
385    topology: &RegionSetLayout,
386) -> Result<(), BindingError> {
387    if layout.matches_topology(topology) {
388        Ok(())
389    } else {
390        Err(BindingError::TopologyMismatch)
391    }
392}
393
394fn record<T, M>(
395    _owner: &M,
396    base: NonNull<u8>,
397    offset: usize,
398    available: usize,
399) -> Result<&T, BindingError> {
400    if available < size_of::<T>() {
401        return Err(BindingError::Layout(LayoutError::RangeOutOfBounds));
402    }
403    // SAFETY: offset was checked against the validated mapping; the witness
404    // retains the allocation and provenance. The returned lifetime is narrowed
405    // immediately by the caller to its borrow of the owning region.
406    let pointer = unsafe { base.as_ptr().add(offset) }.cast::<T>();
407    if !(pointer as usize).is_multiple_of(align_of::<T>()) {
408        return Err(BindingError::MisalignedRecord);
409    }
410    // SAFETY: validation established initialization and `T`'s field offsets;
411    // alignment and complete record bounds were checked above.
412    Ok(unsafe { &*pointer })
413}
414
415#[cfg(test)]
416#[path = "mapping_test.rs"]
417mod tests;