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    /// Validated mapping does not belong to the supplied composed topology.
72    TopologyMismatch,
73    /// Composed topology has no exact route for the requested target slot.
74    MissingRoute {
75        /// Producer role requested from the bound topology.
76        target: RoleId,
77        /// Producer slot requested from the bound topology.
78        slot: u32,
79    },
80}
81
82impl fmt::Display for BindingError {
83    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
84        write!(formatter, "mapping binding failed: {self:?}")
85    }
86}
87
88#[cfg(feature = "std")]
89impl std::error::Error for BindingError {}
90
91impl From<LayoutError> for BindingError {
92    fn from(value: LayoutError) -> Self {
93        Self::Layout(value)
94    }
95}
96
97impl From<SlotError> for BindingError {
98    fn from(value: SlotError) -> Self {
99        Self::Slot(value)
100    }
101}
102
103/// Mapping-lifetime owner that can mint acquire-only capabilities.
104pub struct ReaderRegion<M> {
105    mapping: M,
106    layout: ValidatedRegionLayout,
107    topology: RegionSetLayout,
108}
109
110impl<M: ReadOnlyMapping> ReaderRegion<M> {
111    /// Consumes a platform-minted read-only witness.
112    pub fn new(
113        mapping: M,
114        layout: ValidatedRegionLayout,
115        topology: RegionSetLayout,
116    ) -> Result<Self, BindingError> {
117        validate_mapping_size(mapping.len(), &layout)?;
118        validate_topology(&layout, &topology)?;
119        Ok(Self {
120            mapping,
121            layout,
122            topology,
123        })
124    }
125
126    /// Binds one checked slot without exposing shared bytes.
127    pub fn slot(&self, slot: u32) -> Result<ReaderSlot<'_>, BindingError> {
128        let binding = self.layout.reader_slot_binding(slot)?;
129        let range = self.layout.slot_range(slot)?;
130        let header = record::<SlotMetadata, _>(
131            &self.mapping,
132            self.mapping.base(),
133            range.start,
134            range.len(),
135        )?;
136        // SAFETY: the witness contract supplies provenance, lifetime, exact
137        // validation, and local read-only permission; `record` checked bounds/alignment.
138        Ok(unsafe { ReaderSlot::bind(header, binding) }?)
139    }
140
141    /// Copies one bounded hostile payload and rechecks its publication metadata.
142    ///
143    /// Same-sequence malicious mutation can still produce torn bytes; the
144    /// returned owned buffer must be decoded as hostile input.
145    pub fn copy_payload(&self, slot: u32, expected_sequence: u64) -> Result<Vec<u8>, BindingError> {
146        let observation = self.slot(slot)?.observe(expected_sequence)?;
147        let range = self
148            .layout
149            .slot_payload_range(slot, observation.payload_len())?;
150        let mut owned = Vec::<u8>::new();
151        owned
152            .try_reserve_exact(range.len())
153            .map_err(|_| BindingError::AllocationFailed)?;
154        // SAFETY: the read-only witness keeps this validated range mapped and
155        // readable. Each volatile byte load crosses the externally mutable
156        // memory boundary without forming a shared Rust reference, and each
157        // destination byte is initialized exactly once in disjoint owned memory.
158        unsafe {
159            owned.set_len(range.len());
160            let source = self.mapping.base().as_ptr().add(range.start);
161            let destination = owned.as_mut_ptr();
162            for index in 0..range.len() {
163                destination
164                    .add(index)
165                    .write(core::ptr::read_volatile(source.add(index)));
166            }
167        }
168        self.slot(slot)?.recheck(observation)?;
169        Ok(owned)
170    }
171
172    /// Binds one checked acknowledgement route without exposing shared bytes.
173    pub fn acknowledgement(
174        &self,
175        target: RoleId,
176        slot: u32,
177    ) -> Result<AcknowledgementReader<'_>, BindingError> {
178        let route = self
179            .topology
180            .acknowledgement_route(target, slot)
181            .ok_or(BindingError::MissingRoute { target, slot })?;
182        let binding = self.layout.acknowledgement_reader_binding(route)?;
183        let range = self.layout.acknowledgement_range(route.cell_index())?;
184        let cell = record::<AcknowledgementCell, _>(
185            &self.mapping,
186            self.mapping.base(),
187            range.start,
188            range.len(),
189        )?;
190        // SAFETY: same witness and checked-record proof as `slot`.
191        Ok(unsafe { AcknowledgementReader::bind(cell, binding) })
192    }
193}
194
195/// Mapping-lifetime owner that prevents duplicate safe writer binding.
196pub struct WriterRegion<M> {
197    mapping: M,
198    layout: ValidatedRegionLayout,
199    topology: RegionSetLayout,
200}
201
202impl<M: SoleWriterMapping> WriterRegion<M> {
203    /// Consumes a platform-minted unique writer witness.
204    pub fn new(
205        mapping: M,
206        layout: ValidatedRegionLayout,
207        topology: RegionSetLayout,
208    ) -> Result<Self, BindingError> {
209        validate_mapping_size(mapping.len(), &layout)?;
210        validate_topology(&layout, &topology)?;
211        Ok(Self {
212            mapping,
213            layout,
214            topology,
215        })
216    }
217
218    /// Exclusively binds one checked producer slot.
219    pub fn slot(&mut self, slot: u32) -> Result<WriterSlot<'_>, BindingError> {
220        let target = self.layout.role();
221        let route = self
222            .topology
223            .acknowledgement_route(target, slot)
224            .ok_or(BindingError::MissingRoute { target, slot })?;
225        let binding = self.layout.writer_slot_binding(route)?;
226        let range = self.layout.slot_range(route.slot_index())?;
227        let header = record::<SlotMetadata, _>(
228            &self.mapping,
229            self.mapping.base(),
230            range.start,
231            range.len(),
232        )?;
233        // SAFETY: consuming the unique witness and borrowing `self` mutably
234        // prevent a second safe writer capability while this borrow is live.
235        Ok(unsafe { WriterSlot::bind(header, binding) }?)
236    }
237
238    /// Copies an owned caller payload into a checked slot and publishes it.
239    pub fn publish(
240        &mut self,
241        slot: u32,
242        sequence: u64,
243        acknowledgement: Option<AcknowledgementObservation>,
244        payload: &[u8],
245    ) -> Result<(), BindingError> {
246        let payload_len =
247            u32::try_from(payload.len()).map_err(|_| BindingError::PayloadLengthOverflow)?;
248        let range = self.layout.slot_payload_range(slot, payload_len)?;
249        let base = self.mapping.base();
250        let mut bound_slot = self.slot(slot)?;
251        let reservation = bound_slot.prepare_publish(sequence, acknowledgement)?;
252        // SAFETY: the unique writer witness and `&mut self` exclude other
253        // writers; the checked payload range is disjoint from slot metadata.
254        unsafe {
255            core::ptr::copy_nonoverlapping(
256                payload.as_ptr(),
257                base.as_ptr().add(range.start),
258                payload.len(),
259            );
260        }
261        reservation.publish(payload_len)?;
262        Ok(())
263    }
264
265    /// Exclusively binds one checked acknowledgement cell.
266    pub fn acknowledgement(
267        &mut self,
268        target: RoleId,
269        slot: u32,
270    ) -> Result<AcknowledgementWriter<'_>, BindingError> {
271        let route = self
272            .topology
273            .acknowledgement_route(target, slot)
274            .ok_or(BindingError::MissingRoute { target, slot })?;
275        let binding = self.layout.acknowledgement_writer_binding(route)?;
276        let range = self.layout.acknowledgement_range(route.cell_index())?;
277        let cell = record::<AcknowledgementCell, _>(
278            &self.mapping,
279            self.mapping.base(),
280            range.start,
281            range.len(),
282        )?;
283        // SAFETY: same unique witness and exclusive-borrow proof as `slot`.
284        Ok(unsafe { AcknowledgementWriter::bind(cell, binding) })
285    }
286}
287
288fn validate_mapping_size(
289    actual: usize,
290    layout: &ValidatedRegionLayout,
291) -> Result<(), BindingError> {
292    if actual == layout.mapping_size() {
293        Ok(())
294    } else {
295        Err(BindingError::MappingSizeMismatch {
296            expected: layout.mapping_size(),
297            actual,
298        })
299    }
300}
301
302fn validate_topology(
303    layout: &ValidatedRegionLayout,
304    topology: &RegionSetLayout,
305) -> Result<(), BindingError> {
306    if layout.matches_topology(topology) {
307        Ok(())
308    } else {
309        Err(BindingError::TopologyMismatch)
310    }
311}
312
313fn record<T, M>(
314    _owner: &M,
315    base: NonNull<u8>,
316    offset: usize,
317    available: usize,
318) -> Result<&T, BindingError> {
319    if available < size_of::<T>() {
320        return Err(BindingError::Layout(LayoutError::RangeOutOfBounds));
321    }
322    // SAFETY: offset was checked against the validated mapping; the witness
323    // retains the allocation and provenance. The returned lifetime is narrowed
324    // immediately by the caller to its borrow of the owning region.
325    let pointer = unsafe { base.as_ptr().add(offset) }.cast::<T>();
326    if !(pointer as usize).is_multiple_of(align_of::<T>()) {
327        return Err(BindingError::MisalignedRecord);
328    }
329    // SAFETY: validation established initialization and `T`'s field offsets;
330    // alignment and complete record bounds were checked above.
331    Ok(unsafe { &*pointer })
332}
333
334#[cfg(test)]
335#[path = "mapping_test.rs"]
336mod tests;