nucleus/security/
capabilities.rs1use crate::error::{NucleusError, Result};
2use caps::{CapSet, Capability, CapsHashSet};
3use tracing::{debug, info};
4
5pub struct CapabilityManager {
7 phase: CapPhase,
8}
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq)]
20enum CapPhase {
21 Initial,
23 BoundingDropped,
25 Dropped,
27}
28
29#[derive(Debug, Clone, PartialEq, Eq)]
30pub struct CapabilitySets {
31 pub bounding: Vec<Capability>,
32 pub permitted: Vec<Capability>,
33 pub effective: Vec<Capability>,
34 pub inheritable: Vec<Capability>,
35 pub ambient: Vec<Capability>,
36}
37
38impl CapabilityManager {
39 pub fn new() -> Self {
40 Self {
41 phase: CapPhase::Initial,
42 }
43 }
44
45 pub fn drop_bounding_set(&mut self) -> Result<()> {
54 if self.phase != CapPhase::Initial {
55 debug!("Bounding set already dropped, skipping");
56 return Ok(());
57 }
58
59 info!("Phase 1: dropping bounding set and ambient/inheritable caps");
60
61 for cap in caps::all() {
64 if let Err(e) = caps::drop(None, CapSet::Bounding, cap) {
65 debug!(
66 "Failed to drop bounding cap {:?}: {} (may not be present)",
67 cap, e
68 );
69 }
70 }
71
72 let bounding = caps::read(None, CapSet::Bounding).map_err(|e| {
74 NucleusError::CapabilityError(format!("Failed to read bounding set after drop: {}", e))
75 })?;
76 if !bounding.is_empty() {
77 let leaked: Vec<String> = bounding.iter().map(|c| format!("{:?}", c)).collect();
78 return Err(NucleusError::CapabilityError(format!(
79 "Bounding set still contains capabilities after drop: [{}]",
80 leaked.join(", ")
81 )));
82 }
83
84 caps::clear(None, CapSet::Ambient).map_err(|e| {
86 NucleusError::CapabilityError(format!("Failed to clear ambient caps: {}", e))
87 })?;
88
89 caps::clear(None, CapSet::Inheritable).map_err(|e| {
91 NucleusError::CapabilityError(format!("Failed to clear inheritable caps: {}", e))
92 })?;
93
94 self.phase = CapPhase::BoundingDropped;
98 info!("Phase 1 complete: bounding/ambient/inheritable cleared, effective/permitted retained for identity switch");
99
100 Ok(())
101 }
102
103 pub fn finalize_drop(&mut self) -> Result<()> {
112 if self.phase == CapPhase::Dropped {
113 debug!("Capabilities already fully dropped, skipping");
114 return Ok(());
115 }
116
117 if self.phase == CapPhase::Initial {
118 self.drop_bounding_set()?;
120 }
121
122 info!("Phase 2: clearing permitted and effective caps");
123
124 caps::clear(None, CapSet::Permitted).map_err(|e| {
125 NucleusError::CapabilityError(format!("Failed to clear permitted caps: {}", e))
126 })?;
127
128 caps::clear(None, CapSet::Effective).map_err(|e| {
129 NucleusError::CapabilityError(format!("Failed to clear effective caps: {}", e))
130 })?;
131
132 self.phase = CapPhase::Dropped;
133 info!("Successfully dropped all capabilities (including bounding set)");
134
135 Ok(())
136 }
137
138 pub fn drop_all(&mut self) -> Result<()> {
143 self.drop_bounding_set()?;
144 self.finalize_drop()
145 }
146
147 pub fn drop_except(&mut self, keep: &[Capability]) -> Result<()> {
152 if self.phase == CapPhase::Dropped {
153 debug!("Capabilities already dropped, skipping");
154 return Ok(());
155 }
156
157 info!("Dropping capabilities except: {:?}", keep);
158
159 let all_caps = caps::all();
160
161 for cap in &all_caps {
163 if !keep.contains(cap) {
164 if let Err(e) = caps::drop(None, CapSet::Bounding, *cap) {
165 debug!(
166 "Failed to drop bounding cap {:?}: {} (may not be present)",
167 cap, e
168 );
169 }
170 }
171 }
172
173 caps::clear(None, CapSet::Ambient).map_err(|e| {
175 NucleusError::CapabilityError(format!("Failed to clear ambient caps: {}", e))
176 })?;
177
178 for cap in &all_caps {
180 if !keep.contains(cap) {
181 caps::drop(None, CapSet::Inheritable, *cap).map_err(|e| {
182 NucleusError::CapabilityError(format!("Failed to drop {cap:?}: {e}"))
183 })?;
184
185 caps::drop(None, CapSet::Permitted, *cap).map_err(|e| {
186 NucleusError::CapabilityError(format!("Failed to drop {cap:?}: {e}"))
187 })?;
188
189 caps::drop(None, CapSet::Effective, *cap).map_err(|e| {
190 NucleusError::CapabilityError(format!("Failed to drop {cap:?}: {e}"))
191 })?;
192 }
193 }
194
195 self.phase = CapPhase::Dropped;
196 info!("Successfully dropped capabilities");
197
198 Ok(())
199 }
200
201 pub fn apply_sets(&mut self, sets: &CapabilitySets) -> Result<()> {
206 if self.phase == CapPhase::Dropped {
207 debug!("Capabilities already dropped, skipping");
208 return Ok(());
209 }
210
211 info!("Applying explicit capability sets");
212
213 for cap in caps::all() {
214 if !sets.bounding.contains(&cap) {
215 if let Err(e) = caps::drop(None, CapSet::Bounding, cap) {
216 debug!(
217 "Failed to drop bounding cap {:?}: {} (may not be present)",
218 cap, e
219 );
220 }
221 }
222 }
223
224 caps::set(None, CapSet::Permitted, &to_caps_hash_set(&sets.permitted)).map_err(|e| {
227 NucleusError::CapabilityError(format!("Failed to set permitted caps: {}", e))
228 })?;
229 caps::set(None, CapSet::Effective, &to_caps_hash_set(&sets.effective)).map_err(|e| {
230 NucleusError::CapabilityError(format!("Failed to set effective caps: {}", e))
231 })?;
232 caps::set(
233 None,
234 CapSet::Inheritable,
235 &to_caps_hash_set(&sets.inheritable),
236 )
237 .map_err(|e| {
238 NucleusError::CapabilityError(format!("Failed to set inheritable caps: {}", e))
239 })?;
240 caps::set(None, CapSet::Ambient, &to_caps_hash_set(&sets.ambient)).map_err(|e| {
241 NucleusError::CapabilityError(format!("Failed to set ambient caps: {}", e))
242 })?;
243
244 self.phase = CapPhase::Dropped;
245 info!("Successfully applied capability sets");
246 Ok(())
247 }
248
249 pub fn is_dropped(&self) -> bool {
251 self.phase == CapPhase::Dropped
252 }
253
254 pub fn verify_no_namespace_caps(production: bool) -> Result<()> {
261 use caps::Capability;
262 let ns_caps = [
263 Capability::CAP_SYS_ADMIN,
264 Capability::CAP_NET_ADMIN,
265 Capability::CAP_SYS_PTRACE,
266 ];
267 let effective = caps::read(None, CapSet::Effective).map_err(|e| {
268 NucleusError::CapabilityError(format!("Failed to read effective caps: {}", e))
269 })?;
270 let mut leaked = Vec::new();
271 for cap in &ns_caps {
272 if effective.contains(cap) {
273 leaked.push(format!("{:?}", cap));
274 }
275 }
276 if !leaked.is_empty() {
277 let msg = format!(
278 "SEC-CLONE3: namespace-creating capabilities still present after drop: [{}]. \
279 clone3 syscall is allowed without argument filtering – these caps \
280 must be absent to prevent namespace escape.",
281 leaked.join(", ")
282 );
283 if production {
284 return Err(NucleusError::CapabilityError(msg));
285 }
286 tracing::warn!("{}", msg);
287 }
288 Ok(())
289 }
290}
291
292impl Default for CapabilityManager {
293 fn default() -> Self {
294 Self::new()
295 }
296}
297
298fn to_caps_hash_set(caps_list: &[Capability]) -> CapsHashSet {
299 caps_list.iter().copied().collect()
300}
301
302#[cfg(test)]
303mod tests {
304 use super::*;
305
306 #[test]
307 fn test_capability_manager_initial_state() {
308 let mgr = CapabilityManager::new();
309 assert!(!mgr.is_dropped());
310 }
311
312 #[test]
313 fn test_drop_idempotent() {
314 let mut mgr = CapabilityManager::new();
315 match mgr.drop_all() {
318 Ok(()) => {
319 assert!(mgr.is_dropped());
320 let result = mgr.drop_all();
322 assert!(result.is_ok());
323 assert!(mgr.is_dropped());
324 }
325 Err(_) => {
326 }
329 }
330 }
331
332 #[test]
333 fn test_two_phase_drop() {
334 let mut mgr = CapabilityManager::new();
335 if let Ok(()) = mgr.drop_bounding_set() {
337 assert!(!mgr.is_dropped()); if let Ok(()) = mgr.finalize_drop() {
339 assert!(mgr.is_dropped())
340 }
341 }
343 }
344}