native_ipc/binding.rs
1//! Safe audited binding from committed active mappings to core capabilities.
2//!
3//! This module is the safe, fully audited alternative to the feature-gated
4//! `raw-pointer` escape. It converts a committed [`ActiveReader`] or
5//! [`ActiveWriter`] into the matching `native-ipc-core` capability owner
6//! ([`ReaderRegion`]/[`WriterRegion`]) by consuming the active mapping into a
7//! witness the core boundary trusts. Running the audited core protocol over a
8//! session-transferred region needs no consumer unsafe code and no
9//! `raw-pointer` feature on this path.
10//!
11//! # Witness soundness
12//!
13//! An active mapping is uniquely owned and cannot be cloned. Its local native
14//! view is released only by the owner's own `Drop`; session poison or close
15//! gates the safe accessors but never unmaps the view. Moving the consumed
16//! active value inside a witness therefore keeps the whole `base..base+len`
17//! extent mapped and initialized — and, for the read side, OS-enforced
18//! read-only — for the entire witness lifetime. `len` is the mapping's
19//! validated logical extent ([`ActiveReader::len`]/[`ActiveWriter::len`]); the
20//! page-rounded tail beyond it is deliberately excluded, matching the range the
21//! [`ValidatedRegionLayout`] was validated over.
22//!
23//! After the peer session ends the peer is gone and the bytes are frozen or
24//! stale: read witnesses still observe only hostile, memory-safe bytes, and
25//! write witnesses simply publish to nobody. Liveness re-checking is
26//! deliberately not part of the witness contract; a consumer that needs it
27//! keeps the owning session handle and quiesces before dropping the witness.
28//!
29//! [`ActiveReader`]: crate::active::ActiveReader
30//! [`ActiveReader::len`]: crate::active::ActiveReader::len
31//! [`ActiveWriter`]: crate::active::ActiveWriter
32//! [`ActiveWriter::len`]: crate::active::ActiveWriter::len
33//! [`ReaderRegion`]: crate::core::mapping::ReaderRegion
34//! [`WriterRegion`]: crate::core::mapping::WriterRegion
35//! [`ValidatedRegionLayout`]: crate::core::layout::ValidatedRegionLayout
36
37use crate::active::{ActiveReader, ActiveWriter};
38use crate::core::layout::{RegionSetLayout, ValidatedRegionLayout};
39use crate::core::mapping::{
40 BindingError, ReadOnlyMapping, ReaderRegion, SoleWriterMapping, WriterRegion,
41};
42use core::fmt;
43use core::ptr::NonNull;
44
45/// Read-only witness that owns its consumed active mapping.
46///
47/// A region bound over this witness is a unique capability; neither the witness
48/// nor the region it backs can be duplicated:
49///
50/// ```compile_fail
51/// use native_ipc::binding::BoundReadMapping;
52/// use native_ipc::core::mapping::ReaderRegion;
53/// fn duplicate(
54/// region: ReaderRegion<BoundReadMapping>,
55/// ) -> (ReaderRegion<BoundReadMapping>, ReaderRegion<BoundReadMapping>) {
56/// let copy = region.clone();
57/// (region, copy)
58/// }
59/// ```
60pub struct BoundReadMapping {
61 reader: ActiveReader,
62 base: NonNull<u8>,
63}
64
65// SAFETY: the witness owns an `ActiveReader`, which is itself `Send + Sync`, and
66// the cached `base` is a copy of that mapping's own base that can never outlive
67// the owned reader. Moving or sharing the witness across threads only moves or
68// shares the reader it already permits, so the markers grant no authority the
69// inner mapping did not already have.
70unsafe impl Send for BoundReadMapping {}
71unsafe impl Sync for BoundReadMapping {}
72
73// SAFETY: the owned `ActiveReader` uniquely owns exactly one page-aligned,
74// OS-enforced read-only native view and releases it only in its own `Drop`;
75// session poison or close gates the safe accessors without unmapping, so the
76// view stays mapped and initialized for as long as this witness lives. `base`
77// is that mapping's own base carrying its allocation provenance, and `len`
78// reports its validated logical extent — precisely the bytes the caller's
79// `ValidatedRegionLayout` was validated over. Peer mutation may race, but the
80// core boundary only ever performs volatile loads through `base` and never
81// forms a shared reference, so exposing this witness cannot violate memory
82// safety.
83unsafe impl ReadOnlyMapping for BoundReadMapping {
84 fn base(&self) -> NonNull<u8> {
85 self.base
86 }
87
88 fn len(&self) -> usize {
89 self.reader.len()
90 }
91}
92
93/// Sole-writer witness that owns its consumed active mapping.
94///
95/// Like the [`ActiveWriter`] it owns, a bound writer may move between threads
96/// but cannot be shared between them:
97///
98/// ```compile_fail
99/// use native_ipc::binding::BoundWriteMapping;
100/// use native_ipc::core::mapping::WriterRegion;
101/// fn assert_sync<T: Sync>() {}
102/// assert_sync::<WriterRegion<BoundWriteMapping>>();
103/// ```
104pub struct BoundWriteMapping {
105 writer: ActiveWriter,
106 base: NonNull<u8>,
107}
108
109// SAFETY: the witness owns an `ActiveWriter`, which is `Send` but deliberately
110// not `Sync`; moving it between threads transfers the single writable view
111// without ever aliasing it, and the cached `base` cannot outlive that owned
112// writer. Matching the inner mapping, the witness is `Send` only, so no shared
113// cross-thread writer alias becomes reachable.
114unsafe impl Send for BoundWriteMapping {}
115
116// SAFETY: the owned `ActiveWriter` is, by construction, the region's only
117// writable native view: it is non-cloneable, holds sole store authority, and
118// releases its page-aligned view only in its own `Drop`. Session poison or
119// close gates the safe accessors without unmapping, so the view stays mapped
120// and writable for the witness lifetime. `base` is that mapping's own base with
121// allocation provenance and `len` is its validated logical extent, exactly the
122// range the `ValidatedRegionLayout` was validated over. While a `WriterRegion`
123// owns this witness, safe code cannot recover a second writer for the region.
124unsafe impl SoleWriterMapping for BoundWriteMapping {
125 fn base(&self) -> NonNull<u8> {
126 self.base
127 }
128
129 fn len(&self) -> usize {
130 self.writer.len()
131 }
132}
133
134impl BoundReadMapping {
135 /// Releases the witness and returns the owned active mapping unchanged.
136 pub fn into_active(self) -> ActiveReader {
137 self.reader
138 }
139}
140
141impl BoundWriteMapping {
142 /// Releases the witness and returns the owned active mapping unchanged.
143 pub fn into_active(self) -> ActiveWriter {
144 self.writer
145 }
146}
147
148/// A rejected bind that returns the consumed active mapping to its caller.
149///
150/// The bind boundary consumes the active mapping by value; on rejection this
151/// carrier hands the exact same value back so the caller recovers it instead of
152/// losing the committed mapping.
153pub struct BindRejected<T> {
154 /// Reason the validated layout could not bind to this mapping.
155 pub error: BindingError,
156 value: T,
157}
158
159impl<T> BindRejected<T> {
160 /// Recovers the consumed active mapping unchanged.
161 pub fn into_inner(self) -> T {
162 self.value
163 }
164}
165
166// The recovered active mapping is deliberately opaque, so this does not require
167// `T: Debug`; only the binding error is reported.
168impl<T> fmt::Debug for BindRejected<T> {
169 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
170 formatter
171 .debug_struct("BindRejected")
172 .field("error", &self.error)
173 .finish_non_exhaustive()
174 }
175}
176
177impl ActiveReader {
178 /// Consumes this committed mapping into an audited core read capability.
179 ///
180 /// On rejection the returned [`BindRejected`] carries this exact reader back
181 /// through [`BindRejected::into_inner`].
182 pub fn bind(
183 self,
184 layout: ValidatedRegionLayout,
185 topology: RegionSetLayout,
186 ) -> Result<ReaderRegion<BoundReadMapping>, Box<BindRejected<ActiveReader>>> {
187 let base = self.payload_base();
188 let witness = BoundReadMapping { reader: self, base };
189 ReaderRegion::new(witness, layout, topology).map_err(|(witness, error)| {
190 Box::new(BindRejected {
191 error,
192 value: witness.into_active(),
193 })
194 })
195 }
196}
197
198impl ActiveWriter {
199 /// Consumes this committed mapping into an audited core write capability.
200 ///
201 /// On rejection the returned [`BindRejected`] carries this exact writer back
202 /// through [`BindRejected::into_inner`].
203 pub fn bind(
204 mut self,
205 layout: ValidatedRegionLayout,
206 topology: RegionSetLayout,
207 ) -> Result<WriterRegion<BoundWriteMapping>, Box<BindRejected<ActiveWriter>>> {
208 let base = self.payload_base_mut();
209 let witness = BoundWriteMapping { writer: self, base };
210 WriterRegion::new(witness, layout, topology).map_err(|(witness, error)| {
211 Box::new(BindRejected {
212 error,
213 value: witness.into_active(),
214 })
215 })
216 }
217}
218
219#[cfg(test)]
220#[path = "binding_test.rs"]
221mod tests;