Skip to main content

rvm_cap/
table.rs

1//! Capability table implementation.
2//!
3//! Each partition has a capability table that stores its held capabilities.
4//! The table uses a fixed-size array with generation counters for stale
5//! handle detection. No allocation in `no_std` environments.
6
7use crate::error::{CapError, CapResult};
8use crate::DEFAULT_CAP_TABLE_CAPACITY;
9use rvm_types::{CapRights, CapToken, CapType, PartitionId};
10
11/// A slot in the capability table.
12///
13/// Each slot holds either a valid capability or is marked as free for reuse.
14/// Generation counters prevent stale handle access after deallocation.
15#[derive(Debug, Clone, Copy, PartialEq, Eq)]
16pub struct CapSlot {
17    /// The capability token (valid when `generation != 0`).
18    pub token: CapToken,
19    /// Generation counter for stale handle detection.
20    ///
21    /// Generation 0 is the **invalid sentinel**: a slot with `generation == 0`
22    /// is empty/free. Live slots always have `generation >= 1`, and the
23    /// counter skips 0 on wrap-around (see [`invalidate`](Self::invalidate)).
24    ///
25    /// # Security note
26    ///
27    /// This is a u32, giving a 2^32 cycle forgery window: if an attacker
28    /// can cause exactly 2^32 allocate/free cycles on a single slot, a
29    /// stale handle could alias a new capability. In practice this is
30    /// infeasible (would require ~4 billion operations on one slot), and
31    /// widening to u64 would double `CapSlot` size and break the memory
32    /// layout. Accepted as a low-severity residual risk.
33    pub generation: u32,
34    /// The partition that owns this capability.
35    pub owner: PartitionId,
36    /// Delegation depth (0 = root capability).
37    pub depth: u8,
38    /// Parent slot index (`u32::MAX` if root).
39    pub parent_index: u32,
40    /// Badge value for identifying the granting chain.
41    pub badge: u64,
42}
43
44impl CapSlot {
45    /// Creates an empty (invalid) slot.
46    ///
47    /// Empty slots have `generation == 0`, which is the invalid sentinel.
48    #[inline]
49    #[must_use]
50    const fn empty() -> Self {
51        Self {
52            token: CapToken::new(0, CapType::Region, CapRights::empty(), 0),
53            generation: 0,
54            owner: PartitionId::new(0),
55            depth: 0,
56            parent_index: u32::MAX,
57            badge: 0,
58        }
59    }
60
61    /// Returns true if this slot is currently valid (in use).
62    #[inline]
63    #[must_use]
64    pub const fn is_valid(&self) -> bool {
65        self.generation != 0
66    }
67
68    /// Returns true if this slot matches the given generation.
69    #[inline]
70    #[must_use]
71    pub const fn matches(&self, generation: u32) -> bool {
72        self.is_valid() && self.generation == generation
73    }
74
75    /// Invalidates this slot, bumping the generation counter for the
76    /// next allocation and then clearing it to 0 (the free sentinel).
77    ///
78    /// The bumped generation is stored in `parent_index` (unused while
79    /// the slot is free) so that the next `insert_*` call can recover it.
80    ///
81    /// # Security
82    ///
83    /// Generation 0 is the invalid sentinel. The counter skips 0 on
84    /// wrap-around so that a re-allocated slot never gets generation 0.
85    #[inline]
86    pub fn invalidate(&mut self) {
87        let next_gen = self.generation.wrapping_add(1);
88        // Skip generation 0 (the free sentinel) to prevent aliasing.
89        let safe_gen = if next_gen == 0 { 1 } else { next_gen };
90        // Stash the next generation in parent_index while the slot is free.
91        self.parent_index = safe_gen;
92        // Mark the slot as free.
93        self.generation = 0;
94    }
95
96    /// Recover the next generation counter for a free slot.
97    ///
98    /// For fresh (never-used) slots this returns 1 (since generation 0
99    /// is the invalid sentinel). For previously-invalidated slots, the
100    /// stashed value from `parent_index` is returned.
101    #[inline]
102    #[must_use]
103    const fn next_generation(&self) -> u32 {
104        // Fresh slots have parent_index == u32::MAX and generation == 0.
105        // Invalidated slots have the next-gen stashed in parent_index.
106        if self.generation != 0 {
107            // Slot is occupied -- shouldn't be called, but return current.
108            self.generation
109        } else if self.parent_index == u32::MAX {
110            // Fresh slot, never allocated. First valid generation is 1.
111            1
112        } else {
113            // Previously invalidated: parent_index holds the stashed gen.
114            self.parent_index
115        }
116    }
117}
118
119/// Fixed-size capability table for a partition.
120///
121/// Uses const generic `N` for the maximum number of capability slots.
122/// No heap allocation: backed by a `[CapSlot; N]` array.
123pub struct CapabilityTable<const N: usize = DEFAULT_CAP_TABLE_CAPACITY> {
124    /// The slot array.
125    slots: [CapSlot; N],
126    /// Number of currently valid entries.
127    count: usize,
128    /// Hint for the next free slot (optimization).
129    free_hint: usize,
130}
131
132impl<const N: usize> core::fmt::Debug for CapabilityTable<N> {
133    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
134        f.debug_struct("CapabilityTable")
135            .field("count", &self.count)
136            .field("capacity", &N)
137            .finish_non_exhaustive()
138    }
139}
140
141impl<const N: usize> CapabilityTable<N> {
142    /// Creates a new empty capability table.
143    #[inline]
144    #[must_use]
145    pub const fn new() -> Self {
146        Self {
147            slots: [CapSlot::empty(); N],
148            count: 0,
149            free_hint: 0,
150        }
151    }
152
153    /// Returns the table capacity.
154    #[inline]
155    #[must_use]
156    pub const fn capacity(&self) -> usize {
157        N
158    }
159
160    /// Returns the number of valid entries.
161    #[inline]
162    #[must_use]
163    pub const fn len(&self) -> usize {
164        self.count
165    }
166
167    /// Returns true if the table has no valid entries.
168    #[inline]
169    #[must_use]
170    pub const fn is_empty(&self) -> bool {
171        self.count == 0
172    }
173
174    /// Returns true if the table is full.
175    #[inline]
176    #[must_use]
177    pub const fn is_full(&self) -> bool {
178        self.count >= N
179    }
180
181    /// Inserts a root capability. Returns `(index, generation)`.
182    ///
183    /// # Errors
184    ///
185    /// Returns [`CapError::TableFull`] if no free slot is available.
186    #[allow(clippy::cast_possible_truncation)]
187    pub fn insert_root(
188        &mut self,
189        token: CapToken,
190        owner: PartitionId,
191        badge: u64,
192    ) -> CapResult<(u32, u32)> {
193        let index = self.find_free_slot()?;
194        let generation = self.slots[index].next_generation();
195
196        self.slots[index] = CapSlot {
197            token,
198            generation,
199            owner,
200            depth: 0,
201            parent_index: u32::MAX,
202            badge,
203        };
204        self.count += 1;
205
206        Ok((index as u32, generation))
207    }
208
209    /// Inserts a derived capability. Returns `(index, generation)`.
210    ///
211    /// # Errors
212    ///
213    /// Returns [`CapError::TableFull`] if no free slot is available.
214    #[allow(clippy::cast_possible_truncation)]
215    pub fn insert_derived(
216        &mut self,
217        token: CapToken,
218        owner: PartitionId,
219        depth: u8,
220        parent_index: u32,
221        badge: u64,
222    ) -> CapResult<(u32, u32)> {
223        let index = self.find_free_slot()?;
224        let generation = self.slots[index].next_generation();
225
226        self.slots[index] = CapSlot {
227            token,
228            generation,
229            owner,
230            depth,
231            parent_index,
232            badge,
233        };
234        self.count += 1;
235
236        Ok((index as u32, generation))
237    }
238
239    /// Looks up a slot by index and generation.
240    ///
241    /// # Errors
242    ///
243    /// Returns [`CapError::InvalidHandle`] if the index is out of bounds or the slot is empty.
244    /// Returns [`CapError::StaleHandle`] if the generation does not match.
245    #[inline]
246    pub fn lookup(&self, index: u32, generation: u32) -> CapResult<&CapSlot> {
247        let idx = index as usize;
248        if idx >= N {
249            return Err(CapError::InvalidHandle);
250        }
251        let slot = &self.slots[idx];
252        if !slot.is_valid() {
253            return Err(CapError::InvalidHandle);
254        }
255        if slot.generation != generation {
256            return Err(CapError::StaleHandle);
257        }
258        Ok(slot)
259    }
260
261    /// Looks up a slot mutably by index and generation.
262    ///
263    /// # Errors
264    ///
265    /// Returns [`CapError::InvalidHandle`] if the index is out of bounds or the slot is empty.
266    /// Returns [`CapError::StaleHandle`] if the generation does not match.
267    pub fn lookup_mut(&mut self, index: u32, generation: u32) -> CapResult<&mut CapSlot> {
268        let idx = index as usize;
269        if idx >= N {
270            return Err(CapError::InvalidHandle);
271        }
272        let slot = &mut self.slots[idx];
273        if !slot.is_valid() {
274            return Err(CapError::InvalidHandle);
275        }
276        if slot.generation != generation {
277            return Err(CapError::StaleHandle);
278        }
279        Ok(slot)
280    }
281
282    /// Removes a capability by index and generation.
283    ///
284    /// # Errors
285    ///
286    /// Returns [`CapError::InvalidHandle`] if the index is out of bounds or the slot is empty.
287    /// Returns [`CapError::StaleHandle`] if the generation does not match.
288    pub fn remove(&mut self, index: u32, generation: u32) -> CapResult<()> {
289        let idx = index as usize;
290        if idx >= N {
291            return Err(CapError::InvalidHandle);
292        }
293        let slot = &mut self.slots[idx];
294        if !slot.is_valid() {
295            return Err(CapError::InvalidHandle);
296        }
297        if slot.generation != generation {
298            return Err(CapError::StaleHandle);
299        }
300        slot.invalidate();
301        self.count -= 1;
302        if idx < self.free_hint {
303            self.free_hint = idx;
304        }
305        Ok(())
306    }
307
308    /// Invalidates a slot by index without generation check (internal revocation).
309    pub(crate) fn force_invalidate(&mut self, index: u32) {
310        let idx = index as usize;
311        if idx < N && self.slots[idx].is_valid() {
312            self.slots[idx].invalidate();
313            self.count -= 1;
314            if idx < self.free_hint {
315                self.free_hint = idx;
316            }
317        }
318    }
319
320    /// Returns an iterator over all valid entries as `(index, &CapSlot)`.
321    #[allow(clippy::cast_possible_truncation)]
322    pub fn iter(&self) -> impl Iterator<Item = (u32, &CapSlot)> {
323        self.slots
324            .iter()
325            .enumerate()
326            .filter(|(_, s)| s.is_valid())
327            // Safe: N <= u32::MAX in practice (capped at 256).
328            .map(|(i, s)| (i as u32, s))
329    }
330
331    /// Finds a free slot, starting from `free_hint`.
332    fn find_free_slot(&mut self) -> CapResult<usize> {
333        for i in self.free_hint..N {
334            if !self.slots[i].is_valid() {
335                self.free_hint = i + 1;
336                return Ok(i);
337            }
338        }
339        for i in 0..self.free_hint {
340            if !self.slots[i].is_valid() {
341                self.free_hint = i + 1;
342                return Ok(i);
343            }
344        }
345        Err(CapError::TableFull)
346    }
347}
348
349impl<const N: usize> Default for CapabilityTable<N> {
350    fn default() -> Self {
351        Self::new()
352    }
353}
354
355#[cfg(test)]
356mod tests {
357    use super::*;
358
359    fn test_token(id: u64) -> CapToken {
360        CapToken::new(
361            id,
362            CapType::Region,
363            CapRights::READ.union(CapRights::WRITE),
364            0,
365        )
366    }
367
368    #[test]
369    fn test_insert_and_lookup() {
370        let mut table = CapabilityTable::<16>::new();
371        let owner = PartitionId::new(1);
372        let token = test_token(100);
373
374        let (idx, gen) = table.insert_root(token, owner, 0).unwrap();
375        assert_eq!(table.len(), 1);
376
377        let slot = table.lookup(idx, gen).unwrap();
378        assert_eq!(slot.token.id(), 100);
379        assert_eq!(slot.depth, 0);
380        assert_eq!(slot.parent_index, u32::MAX);
381    }
382
383    #[test]
384    fn test_remove_and_stale() {
385        let mut table = CapabilityTable::<16>::new();
386        let owner = PartitionId::new(1);
387        let token = test_token(200);
388
389        let (idx, gen) = table.insert_root(token, owner, 0).unwrap();
390        table.remove(idx, gen).unwrap();
391        assert_eq!(table.len(), 0);
392        assert!(table.lookup(idx, gen).is_err());
393    }
394
395    #[test]
396    fn test_generation_counter() {
397        let mut table = CapabilityTable::<16>::new();
398        let owner = PartitionId::new(1);
399        let token = test_token(300);
400
401        let (idx, gen1) = table.insert_root(token, owner, 0).unwrap();
402        table.remove(idx, gen1).unwrap();
403
404        let (idx2, gen2) = table.insert_root(token, owner, 0).unwrap();
405        assert_eq!(idx, idx2);
406        assert_ne!(gen1, gen2);
407
408        assert!(table.lookup(idx, gen1).is_err());
409        assert!(table.lookup(idx2, gen2).is_ok());
410    }
411
412    #[test]
413    fn test_table_full() {
414        let mut table = CapabilityTable::<2>::new();
415        let owner = PartitionId::new(1);
416        let token = test_token(400);
417
418        table.insert_root(token, owner, 0).unwrap();
419        table.insert_root(token, owner, 0).unwrap();
420        assert!(table.is_full());
421        assert_eq!(table.insert_root(token, owner, 0), Err(CapError::TableFull));
422    }
423
424    #[test]
425    fn test_insert_derived() {
426        let mut table = CapabilityTable::<16>::new();
427        let owner = PartitionId::new(1);
428        let token = test_token(500);
429
430        let (parent_idx, _) = table.insert_root(token, owner, 0).unwrap();
431        let derived = CapToken::new(501, CapType::Region, CapRights::READ, 0);
432        let (child_idx, child_gen) = table
433            .insert_derived(derived, owner, 1, parent_idx, 42)
434            .unwrap();
435
436        let slot = table.lookup(child_idx, child_gen).unwrap();
437        assert_eq!(slot.depth, 1);
438        assert_eq!(slot.parent_index, parent_idx);
439        assert_eq!(slot.badge, 42);
440    }
441
442    #[test]
443    fn test_iter_valid_entries() {
444        let mut table = CapabilityTable::<16>::new();
445        let owner = PartitionId::new(1);
446
447        table.insert_root(test_token(1), owner, 0).unwrap();
448        table.insert_root(test_token(2), owner, 0).unwrap();
449        table.insert_root(test_token(3), owner, 0).unwrap();
450
451        let count = table.iter().count();
452        assert_eq!(count, 3);
453    }
454}