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::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; the reserved owned destination is disjoint shared memory.
156        unsafe {
157            owned.set_len(range.len());
158            core::ptr::copy_nonoverlapping(
159                self.mapping.base().as_ptr().add(range.start),
160                owned.as_mut_ptr(),
161                range.len(),
162            );
163        }
164        self.slot(slot)?.recheck(observation)?;
165        Ok(owned)
166    }
167
168    /// Binds one checked acknowledgement route without exposing shared bytes.
169    pub fn acknowledgement(
170        &self,
171        target: RoleId,
172        slot: u32,
173    ) -> Result<AcknowledgementReader<'_>, BindingError> {
174        let route = self
175            .topology
176            .acknowledgement_route(target, slot)
177            .ok_or(BindingError::MissingRoute { target, slot })?;
178        let binding = self.layout.acknowledgement_reader_binding(route)?;
179        let range = self.layout.acknowledgement_range(route.cell_index())?;
180        let cell = record::<AcknowledgementCell, _>(
181            &self.mapping,
182            self.mapping.base(),
183            range.start,
184            range.len(),
185        )?;
186        // SAFETY: same witness and checked-record proof as `slot`.
187        Ok(unsafe { AcknowledgementReader::bind(cell, binding) })
188    }
189}
190
191/// Mapping-lifetime owner that prevents duplicate safe writer binding.
192pub struct WriterRegion<M> {
193    mapping: M,
194    layout: ValidatedRegionLayout,
195    topology: RegionSetLayout,
196}
197
198impl<M: SoleWriterMapping> WriterRegion<M> {
199    /// Consumes a platform-minted unique writer witness.
200    pub fn new(
201        mapping: M,
202        layout: ValidatedRegionLayout,
203        topology: RegionSetLayout,
204    ) -> Result<Self, BindingError> {
205        validate_mapping_size(mapping.len(), &layout)?;
206        validate_topology(&layout, &topology)?;
207        Ok(Self {
208            mapping,
209            layout,
210            topology,
211        })
212    }
213
214    /// Exclusively binds one checked producer slot.
215    pub fn slot(&mut self, slot: u32) -> Result<WriterSlot<'_>, BindingError> {
216        let target = self.layout.role();
217        let route = self
218            .topology
219            .acknowledgement_route(target, slot)
220            .ok_or(BindingError::MissingRoute { target, slot })?;
221        let binding = self.layout.writer_slot_binding(route)?;
222        let range = self.layout.slot_range(route.slot_index())?;
223        let header = record::<SlotMetadata, _>(
224            &self.mapping,
225            self.mapping.base(),
226            range.start,
227            range.len(),
228        )?;
229        // SAFETY: consuming the unique witness and borrowing `self` mutably
230        // prevent a second safe writer capability while this borrow is live.
231        Ok(unsafe { WriterSlot::bind(header, binding) }?)
232    }
233
234    /// Copies an owned caller payload into a checked slot and publishes it.
235    pub fn publish(
236        &mut self,
237        slot: u32,
238        sequence: u64,
239        acknowledgement: Option<AcknowledgementObservation>,
240        payload: &[u8],
241    ) -> Result<(), BindingError> {
242        let payload_len =
243            u32::try_from(payload.len()).map_err(|_| BindingError::PayloadLengthOverflow)?;
244        let range = self.layout.slot_payload_range(slot, payload_len)?;
245        let base = self.mapping.base();
246        let mut bound_slot = self.slot(slot)?;
247        let reservation = bound_slot.prepare_publish(sequence, acknowledgement)?;
248        // SAFETY: the unique writer witness and `&mut self` exclude other
249        // writers; the checked payload range is disjoint from slot metadata.
250        unsafe {
251            core::ptr::copy_nonoverlapping(
252                payload.as_ptr(),
253                base.as_ptr().add(range.start),
254                payload.len(),
255            );
256        }
257        reservation.publish(payload_len)?;
258        Ok(())
259    }
260
261    /// Exclusively binds one checked acknowledgement cell.
262    pub fn acknowledgement(
263        &mut self,
264        target: RoleId,
265        slot: u32,
266    ) -> Result<AcknowledgementWriter<'_>, BindingError> {
267        let route = self
268            .topology
269            .acknowledgement_route(target, slot)
270            .ok_or(BindingError::MissingRoute { target, slot })?;
271        let binding = self.layout.acknowledgement_writer_binding(route)?;
272        let range = self.layout.acknowledgement_range(route.cell_index())?;
273        let cell = record::<AcknowledgementCell, _>(
274            &self.mapping,
275            self.mapping.base(),
276            range.start,
277            range.len(),
278        )?;
279        // SAFETY: same unique witness and exclusive-borrow proof as `slot`.
280        Ok(unsafe { AcknowledgementWriter::bind(cell, binding) })
281    }
282}
283
284fn validate_mapping_size(
285    actual: usize,
286    layout: &ValidatedRegionLayout,
287) -> Result<(), BindingError> {
288    if actual == layout.mapping_size() {
289        Ok(())
290    } else {
291        Err(BindingError::MappingSizeMismatch {
292            expected: layout.mapping_size(),
293            actual,
294        })
295    }
296}
297
298fn validate_topology(
299    layout: &ValidatedRegionLayout,
300    topology: &RegionSetLayout,
301) -> Result<(), BindingError> {
302    if layout.matches_topology(topology) {
303        Ok(())
304    } else {
305        Err(BindingError::TopologyMismatch)
306    }
307}
308
309fn record<T, M>(
310    _owner: &M,
311    base: NonNull<u8>,
312    offset: usize,
313    available: usize,
314) -> Result<&T, BindingError> {
315    if available < size_of::<T>() {
316        return Err(BindingError::Layout(LayoutError::RangeOutOfBounds));
317    }
318    // SAFETY: offset was checked against the validated mapping; the witness
319    // retains the allocation and provenance. The returned lifetime is narrowed
320    // immediately by the caller to its borrow of the owning region.
321    let pointer = unsafe { base.as_ptr().add(offset) }.cast::<T>();
322    if !(pointer as usize).is_multiple_of(align_of::<T>()) {
323        return Err(BindingError::MisalignedRecord);
324    }
325    // SAFETY: validation established initialization and `T`'s field offsets;
326    // alignment and complete record bounds were checked above.
327    Ok(unsafe { &*pointer })
328}
329
330#[cfg(test)]
331mod tests {
332    use super::*;
333    use crate::layout::{
334        AcknowledgementRouteSpec, Endpoint, LayoutLimits, RegionSetLayout, RegionSpec, RoleId,
335        ValidationExpectations,
336    };
337    use std::alloc::{Layout, alloc_zeroed, dealloc};
338
339    struct Allocation {
340        base: NonNull<u8>,
341        len: usize,
342    }
343
344    impl Allocation {
345        fn new(len: usize) -> Self {
346            let layout = Layout::from_size_align(len, 64).unwrap();
347            let base = NonNull::new(unsafe { alloc_zeroed(layout) }).unwrap();
348            Self { base, len }
349        }
350        fn bytes_mut(&mut self) -> &mut [u8] {
351            unsafe { core::slice::from_raw_parts_mut(self.base.as_ptr(), self.len) }
352        }
353        fn bytes(&self) -> &[u8] {
354            unsafe { core::slice::from_raw_parts(self.base.as_ptr(), self.len) }
355        }
356    }
357    impl Drop for Allocation {
358        fn drop(&mut self) {
359            unsafe {
360                dealloc(
361                    self.base.as_ptr(),
362                    Layout::from_size_align(self.len, 64).unwrap(),
363                )
364            }
365        }
366    }
367
368    struct ReaderWitness<'a>(&'a Allocation);
369    struct WriterWitness<'a>(&'a mut Allocation);
370    unsafe impl ReadOnlyMapping for ReaderWitness<'_> {
371        fn base(&self) -> NonNull<u8> {
372            self.0.base
373        }
374        fn len(&self) -> usize {
375            self.0.len
376        }
377    }
378    unsafe impl SoleWriterMapping for WriterWitness<'_> {
379        fn base(&self) -> NonNull<u8> {
380            self.0.base
381        }
382        fn len(&self) -> usize {
383            self.0.len
384        }
385    }
386
387    #[test]
388    fn initialize_validate_bind_publish_recheck_and_acknowledge() {
389        let producer = RoleId::new(1).unwrap();
390        let acknowledger = RoleId::new(2).unwrap();
391        let specs = [
392            RegionSpec {
393                role: producer,
394                writer: Endpoint::Initiator,
395                slot_count: 1,
396                payload_bytes: 16,
397                acknowledgement_count: 1,
398            },
399            RegionSpec {
400                role: acknowledger,
401                writer: Endpoint::Responder,
402                slot_count: 1,
403                payload_bytes: 16,
404                acknowledgement_count: 1,
405            },
406        ];
407        let route_specs = [
408            AcknowledgementRouteSpec {
409                owner: acknowledger,
410                target: producer,
411                slot_index: 0,
412                cell_index: 0,
413            },
414            AcknowledgementRouteSpec {
415                owner: producer,
416                target: acknowledger,
417                slot_index: 0,
418                cell_index: 0,
419            },
420        ];
421        let limits = LayoutLimits {
422            maximum_mapping_size: 4096,
423            maximum_slot_count: 2,
424            maximum_acknowledgement_count: 2,
425            maximum_payload_bytes: 64,
426        };
427        let set = RegionSetLayout::calculate([7; 32], 9, &specs, &route_specs, limits).unwrap();
428        let producer_layout = set.region(producer).unwrap();
429        let mut producer_memory = Allocation::new(producer_layout.total_size() as usize);
430        producer_layout
431            .encode_into(producer_memory.bytes_mut())
432            .unwrap();
433        let producer_validated = unsafe {
434            ValidatedRegionLayout::validate(
435                producer_memory.bytes(),
436                ValidationExpectations {
437                    schema_id: [7; 32],
438                    generation: 9,
439                    role: producer,
440                    writer: Endpoint::Initiator,
441                    maximum_mapping_size: 4096,
442                },
443                &set,
444            )
445        }
446        .unwrap();
447
448        let ack_layout = set.region(acknowledger).unwrap();
449        let mut ack_memory = Allocation::new(ack_layout.total_size() as usize);
450        ack_layout.encode_into(ack_memory.bytes_mut()).unwrap();
451        let ack_validated = unsafe {
452            ValidatedRegionLayout::validate(
453                ack_memory.bytes(),
454                ValidationExpectations {
455                    schema_id: [7; 32],
456                    generation: 9,
457                    role: acknowledger,
458                    writer: Endpoint::Responder,
459                    maximum_mapping_size: 4096,
460                },
461                &set,
462            )
463        }
464        .unwrap();
465
466        {
467            let mut writer = WriterRegion::new(
468                WriterWitness(&mut producer_memory),
469                producer_validated.clone(),
470                set.clone(),
471            )
472            .unwrap();
473            writer.publish(0, 1, None, b"ping").unwrap();
474        }
475        let reader = ReaderRegion::new(
476            ReaderWitness(&producer_memory),
477            producer_validated,
478            set.clone(),
479        )
480        .unwrap();
481        let observation = reader.slot(0).unwrap().observe(1).unwrap();
482        reader.slot(0).unwrap().recheck(observation).unwrap();
483        assert_eq!(reader.copy_payload(0, 1).unwrap(), b"ping");
484
485        {
486            let mut ack_writer = WriterRegion::new(
487                WriterWitness(&mut ack_memory),
488                ack_validated.clone(),
489                set.clone(),
490            )
491            .unwrap();
492            ack_writer
493                .acknowledgement(producer, 0)
494                .unwrap()
495                .acknowledge(observation)
496                .unwrap();
497        }
498        let ack_reader = ReaderRegion::new(ReaderWitness(&ack_memory), ack_validated, set).unwrap();
499        let acknowledged = ack_reader.acknowledgement(producer, 0).unwrap().observe();
500        assert_eq!(acknowledged.sequence(), 1);
501        assert_eq!(acknowledged.slot_index(), 0);
502        assert_eq!(acknowledged.cell_index(), 0);
503    }
504}