Skip to main content

zub/namespace/
mapping.rs

1use serde::{Deserialize, Serialize};
2
3/// a single range in a uid/gid mapping
4#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
5pub struct MapEntry {
6    /// start of range inside namespace (logical value)
7    pub inside_start: u32,
8    /// start of range outside namespace (on-disk value)
9    pub outside_start: u32,
10    /// number of ids in this range
11    pub count: u32,
12}
13
14impl MapEntry {
15    pub fn new(inside_start: u32, outside_start: u32, count: u32) -> Self {
16        Self {
17            inside_start,
18            outside_start,
19            count,
20        }
21    }
22
23    /// identity mapping for a single id
24    pub fn identity_single(id: u32) -> Self {
25        Self {
26            inside_start: id,
27            outside_start: id,
28            count: 1,
29        }
30    }
31
32    /// check if an inside id falls within this range
33    pub fn contains_inside(&self, id: u32) -> bool {
34        id >= self.inside_start && id < self.inside_start.saturating_add(self.count)
35    }
36
37    /// check if an outside id falls within this range
38    pub fn contains_outside(&self, id: u32) -> bool {
39        id >= self.outside_start && id < self.outside_start.saturating_add(self.count)
40    }
41}
42
43/// namespace configuration with uid and gid mappings
44#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
45pub struct NsConfig {
46    pub uid_map: Vec<MapEntry>,
47    pub gid_map: Vec<MapEntry>,
48}
49
50impl NsConfig {
51    /// create identity mapping (outside == inside for all ids)
52    /// this is what you get when running as real root outside any namespace
53    pub fn identity() -> Self {
54        Self {
55            uid_map: vec![MapEntry::new(0, 0, u32::MAX)],
56            gid_map: vec![MapEntry::new(0, 0, u32::MAX)],
57        }
58    }
59
60    /// check if this is an identity mapping
61    pub fn is_identity(&self) -> bool {
62        self.uid_map.len() == 1
63            && self.uid_map[0].inside_start == 0
64            && self.uid_map[0].outside_start == 0
65            && self.uid_map[0].count == u32::MAX
66            && self.gid_map.len() == 1
67            && self.gid_map[0].inside_start == 0
68            && self.gid_map[0].outside_start == 0
69            && self.gid_map[0].count == u32::MAX
70    }
71}
72
73/// convert outside (on-disk) id to inside (logical namespace) id
74pub fn outside_to_inside(outside: u32, map: &[MapEntry]) -> Option<u32> {
75    for entry in map {
76        if entry.contains_outside(outside) {
77            return Some(entry.inside_start + (outside - entry.outside_start));
78        }
79    }
80    None
81}
82
83/// convert inside (logical namespace) id to outside (on-disk) id
84pub fn inside_to_outside(inside: u32, map: &[MapEntry]) -> Option<u32> {
85    for entry in map {
86        if entry.contains_inside(inside) {
87            return Some(entry.outside_start + (inside - entry.inside_start));
88        }
89    }
90    None
91}
92
93/// remap an id from one namespace to another
94/// old_outside -> inside (via old_map) -> new_outside (via new_map)
95pub fn remap(old_outside: u32, old_map: &[MapEntry], new_map: &[MapEntry]) -> Option<u32> {
96    let inside = outside_to_inside(old_outside, old_map)?;
97    inside_to_outside(inside, new_map)
98}
99
100/// check if two namespace configs are equivalent (same mappings)
101pub fn mappings_equal(a: &NsConfig, b: &NsConfig) -> bool {
102    a.uid_map == b.uid_map && a.gid_map == b.gid_map
103}
104
105#[cfg(test)]
106mod tests {
107    use super::*;
108
109    #[test]
110    fn test_map_entry_contains() {
111        let entry = MapEntry::new(1, 100000, 65536);
112
113        // inside range
114        assert!(entry.contains_inside(1));
115        assert!(entry.contains_inside(100));
116        assert!(entry.contains_inside(65536));
117        assert!(!entry.contains_inside(0));
118        assert!(!entry.contains_inside(65537));
119
120        // outside range
121        assert!(entry.contains_outside(100000));
122        assert!(entry.contains_outside(100100));
123        assert!(entry.contains_outside(165535));
124        assert!(!entry.contains_outside(99999));
125        assert!(!entry.contains_outside(165536));
126    }
127
128    #[test]
129    fn test_outside_to_inside_simple() {
130        // typical podman unshare mapping:
131        // 0 -> 1000 (root inside maps to user outside)
132        // 1-65536 -> 100000-165535
133        let map = vec![MapEntry::new(0, 1000, 1), MapEntry::new(1, 100000, 65536)];
134
135        // root
136        assert_eq!(outside_to_inside(1000, &map), Some(0));
137
138        // regular user
139        assert_eq!(outside_to_inside(100000, &map), Some(1));
140        assert_eq!(outside_to_inside(100189, &map), Some(190));
141
142        // unmapped
143        assert_eq!(outside_to_inside(999, &map), None);
144        assert_eq!(outside_to_inside(200000, &map), None);
145    }
146
147    #[test]
148    fn test_inside_to_outside_simple() {
149        let map = vec![MapEntry::new(0, 1000, 1), MapEntry::new(1, 100000, 65536)];
150
151        assert_eq!(inside_to_outside(0, &map), Some(1000));
152        assert_eq!(inside_to_outside(1, &map), Some(100000));
153        assert_eq!(inside_to_outside(190, &map), Some(100189));
154
155        // unmapped (beyond range)
156        assert_eq!(inside_to_outside(70000, &map), None);
157    }
158
159    #[test]
160    fn test_remap_between_namespaces() {
161        // machine A: 0->1000, 1-65536->100000-165535
162        let map_a = vec![MapEntry::new(0, 1000, 1), MapEntry::new(1, 100000, 65536)];
163
164        // machine B: 0->2000, 1-65536->200000-265535
165        let map_b = vec![MapEntry::new(0, 2000, 1), MapEntry::new(1, 200000, 65536)];
166
167        // file owned by uid 100189 on machine A (inside uid 190)
168        // should become uid 200189 on machine B
169        assert_eq!(remap(100189, &map_a, &map_b), Some(200189));
170
171        // root file: 1000 on A -> 2000 on B
172        assert_eq!(remap(1000, &map_a, &map_b), Some(2000));
173    }
174
175    #[test]
176    fn test_identity_mapping() {
177        let id = NsConfig::identity();
178        assert!(id.is_identity());
179
180        // everything maps to itself
181        assert_eq!(outside_to_inside(0, &id.uid_map), Some(0));
182        assert_eq!(outside_to_inside(1000, &id.uid_map), Some(1000));
183        assert_eq!(
184            outside_to_inside(u32::MAX - 1, &id.uid_map),
185            Some(u32::MAX - 1)
186        );
187
188        assert_eq!(inside_to_outside(0, &id.uid_map), Some(0));
189        assert_eq!(inside_to_outside(65535, &id.uid_map), Some(65535));
190    }
191
192    #[test]
193    fn test_non_identity() {
194        let ns = NsConfig {
195            uid_map: vec![MapEntry::new(0, 1000, 1)],
196            gid_map: vec![MapEntry::new(0, 1000, 1)],
197        };
198        assert!(!ns.is_identity());
199    }
200
201    #[test]
202    fn test_mappings_equal() {
203        let a = NsConfig {
204            uid_map: vec![MapEntry::new(0, 1000, 1)],
205            gid_map: vec![MapEntry::new(0, 1000, 1)],
206        };
207        let b = NsConfig {
208            uid_map: vec![MapEntry::new(0, 1000, 1)],
209            gid_map: vec![MapEntry::new(0, 1000, 1)],
210        };
211        let c = NsConfig {
212            uid_map: vec![MapEntry::new(0, 2000, 1)],
213            gid_map: vec![MapEntry::new(0, 1000, 1)],
214        };
215
216        assert!(mappings_equal(&a, &b));
217        assert!(!mappings_equal(&a, &c));
218    }
219
220    #[test]
221    fn test_overflow_safety() {
222        // ensure we don't panic on edge cases
223        let entry = MapEntry::new(u32::MAX - 10, 0, 20);
224        assert!(entry.contains_inside(u32::MAX - 10));
225        // saturating_add caps at MAX, so range becomes [MAX-10, MAX) - exclusive upper bound
226        // so MAX-1 is included but MAX is not (it would require checking < MAX which is true for MAX-1)
227        assert!(entry.contains_inside(u32::MAX - 1));
228        // MAX is NOT in the range because saturating_add(MAX-10, 20) = MAX, and MAX < MAX is false
229        assert!(!entry.contains_inside(u32::MAX));
230
231        let entry2 = MapEntry::new(0, u32::MAX - 10, 20);
232        assert!(entry2.contains_outside(u32::MAX - 1));
233        assert!(!entry2.contains_outside(u32::MAX));
234    }
235}