Skip to main content

execgo_runtime/
policy.rs

1use crate::{
2    error::{AppError, AppResult},
3    types::{
4        CapabilityMode, ExecutionPlan, NamespaceConfig, ResourceEnforcementPlan,
5        RuntimeCapabilities, SandboxProfile, SubmitTaskRequest,
6    },
7};
8
9pub fn resolve_execution_plan(
10    request: &SubmitTaskRequest,
11    capabilities: &RuntimeCapabilities,
12    default_mode: CapabilityMode,
13) -> AppResult<ExecutionPlan> {
14    let strict = effective_capability_mode(request, default_mode) == CapabilityMode::Strict
15        || request
16            .control_context
17            .as_ref()
18            .map(|context| context.requires_strict_sandbox)
19            .unwrap_or(false);
20
21    let mut degraded = false;
22    let mut fallback_reasons = Vec::new();
23    let mut effective_sandbox = request.sandbox.clone();
24
25    if matches!(effective_sandbox.profile, SandboxProfile::LinuxSandbox)
26        && !capabilities.sandbox.linux_sandbox
27    {
28        if strict {
29            return Err(AppError::UnsupportedCapability(
30                "sandbox.profile=linux_sandbox is unavailable on this runtime".into(),
31            ));
32        }
33        degraded = true;
34        fallback_reasons
35            .push("linux_sandbox is unavailable; falling back to process sandbox".into());
36        effective_sandbox.profile = SandboxProfile::Process;
37        effective_sandbox.chroot = false;
38        effective_sandbox.rootfs = None;
39        effective_sandbox.namespaces = None;
40    }
41
42    if effective_sandbox.chroot && !capabilities.sandbox.chroot {
43        if strict {
44            return Err(AppError::UnsupportedCapability(
45                "sandbox.chroot is unavailable on this runtime".into(),
46            ));
47        }
48        degraded = true;
49        fallback_reasons.push("chroot is unavailable; running without chroot".into());
50        effective_sandbox.chroot = false;
51        effective_sandbox.rootfs = None;
52    }
53
54    if matches!(effective_sandbox.profile, SandboxProfile::LinuxSandbox) {
55        let requested_namespaces = effective_sandbox.effective_namespaces();
56        let adjusted = NamespaceConfig {
57            mount: namespace_or_fallback(
58                requested_namespaces.mount,
59                capabilities.sandbox.namespaces.mount,
60                strict,
61                &mut degraded,
62                &mut fallback_reasons,
63                "mount",
64            )?,
65            pid: namespace_or_fallback(
66                requested_namespaces.pid,
67                capabilities.sandbox.namespaces.pid,
68                strict,
69                &mut degraded,
70                &mut fallback_reasons,
71                "pid",
72            )?,
73            uts: namespace_or_fallback(
74                requested_namespaces.uts,
75                capabilities.sandbox.namespaces.uts,
76                strict,
77                &mut degraded,
78                &mut fallback_reasons,
79                "uts",
80            )?,
81            ipc: namespace_or_fallback(
82                requested_namespaces.ipc,
83                capabilities.sandbox.namespaces.ipc,
84                strict,
85                &mut degraded,
86                &mut fallback_reasons,
87                "ipc",
88            )?,
89            net: namespace_or_fallback(
90                requested_namespaces.net,
91                capabilities.sandbox.namespaces.net,
92                strict,
93                &mut degraded,
94                &mut fallback_reasons,
95                "net",
96            )?,
97        };
98        effective_sandbox.namespaces = Some(adjusted);
99    }
100
101    let cgroup_enforced = matches!(effective_sandbox.profile, SandboxProfile::LinuxSandbox)
102        && capabilities.resources.cgroup_writable;
103    let cpu_time_enforced =
104        request.limits.cpu_time_sec.is_none() || capabilities.resources.rlimit_cpu;
105    let memory_enforced =
106        request.limits.memory_bytes.is_none() || capabilities.resources.rlimit_memory;
107    let pids_enforced =
108        request.limits.pids_max.is_none() || (cgroup_enforced && capabilities.resources.pids_limit);
109    let oom_detection = request.limits.memory_bytes.is_some()
110        && cgroup_enforced
111        && capabilities.resources.oom_detection;
112
113    if request.limits.cpu_time_sec.is_some() && !cpu_time_enforced {
114        if strict {
115            return Err(AppError::UnsupportedCapability(
116                "cpu_time_sec enforcement is unavailable on this runtime".into(),
117            ));
118        }
119        degraded = true;
120        fallback_reasons.push("cpu_time_sec enforcement is unavailable".into());
121    }
122
123    if request.limits.memory_bytes.is_some() && !memory_enforced {
124        if strict {
125            return Err(AppError::UnsupportedCapability(
126                "memory_bytes enforcement is unavailable on this runtime".into(),
127            ));
128        }
129        degraded = true;
130        fallback_reasons.push("memory_bytes enforcement is unavailable".into());
131    }
132
133    if request.limits.pids_max.is_some() && !pids_enforced {
134        if strict {
135            return Err(AppError::UnsupportedCapability(
136                "pids_max enforcement requires writable cgroup support".into(),
137            ));
138        }
139        degraded = true;
140        fallback_reasons.push("pids_max enforcement requires writable cgroup support".into());
141    }
142
143    Ok(ExecutionPlan {
144        capability_mode: effective_capability_mode(request, default_mode),
145        requested_sandbox: request.sandbox.clone(),
146        effective_sandbox,
147        resource_enforcement: ResourceEnforcementPlan {
148            wall_time_ms: request.limits.wall_time_ms,
149            cpu_time_sec: request.limits.cpu_time_sec,
150            cpu_time_enforced: request.limits.cpu_time_sec.is_some() && cpu_time_enforced,
151            memory_bytes: request.limits.memory_bytes,
152            memory_enforced: request.limits.memory_bytes.is_some() && memory_enforced,
153            pids_max: request.limits.pids_max,
154            pids_enforced: request.limits.pids_max.is_some() && pids_enforced,
155            cgroup_enforced,
156            oom_detection,
157        },
158        degraded,
159        fallback_reasons,
160        capability_warnings: capabilities.warnings.clone(),
161    })
162}
163
164pub fn effective_capability_mode(
165    request: &SubmitTaskRequest,
166    default_mode: CapabilityMode,
167) -> CapabilityMode {
168    request
169        .policy
170        .as_ref()
171        .map(|policy| policy.capability_mode)
172        .unwrap_or(default_mode)
173}
174
175fn namespace_or_fallback(
176    requested: bool,
177    supported: bool,
178    strict: bool,
179    degraded: &mut bool,
180    fallback_reasons: &mut Vec<String>,
181    namespace: &str,
182) -> AppResult<bool> {
183    if !requested {
184        return Ok(false);
185    }
186    if supported {
187        return Ok(true);
188    }
189    if strict {
190        return Err(AppError::UnsupportedCapability(format!(
191            "{namespace} namespace is unavailable on this runtime"
192        )));
193    }
194    *degraded = true;
195    fallback_reasons.push(format!(
196        "{namespace} namespace is unavailable; running without it"
197    ));
198    Ok(false)
199}
200
201#[cfg(test)]
202mod tests {
203    use std::collections::BTreeMap;
204
205    use chrono::Utc;
206
207    use super::*;
208    use crate::types::{
209        ControlContext, ExecutionCapabilities, ExecutionKind, ExecutionSpec, NamespaceCapabilities,
210        ResourceCapabilities, ResourceCapacity, ResourceLimits, RuntimePlatform,
211        SandboxCapabilities, SandboxPolicy, StorageCapabilities, TaskPolicy,
212    };
213
214    fn capabilities(linux_sandbox: bool, cgroup_writable: bool) -> RuntimeCapabilities {
215        RuntimeCapabilities {
216            runtime_id: "test".into(),
217            snapshot_version: "v1".into(),
218            collected_at: Utc::now(),
219            platform: RuntimePlatform {
220                os: "test".into(),
221                arch: "test".into(),
222                containerized: false,
223                kubernetes: false,
224            },
225            execution: ExecutionCapabilities {
226                command: true,
227                script: true,
228                process_group: true,
229            },
230            sandbox: SandboxCapabilities {
231                process: true,
232                linux_sandbox,
233                chroot: linux_sandbox,
234                namespaces: NamespaceCapabilities {
235                    mount: linux_sandbox,
236                    pid: linux_sandbox,
237                    uts: linux_sandbox,
238                    ipc: linux_sandbox,
239                    net: linux_sandbox,
240                },
241            },
242            storage: StorageCapabilities {
243                data_dir_writable: true,
244            },
245            resources: ResourceCapabilities {
246                rlimit_cpu: true,
247                rlimit_memory: true,
248                cgroup_v2: cgroup_writable,
249                cgroup_writable,
250                memory_limit: true,
251                pids_limit: cgroup_writable,
252                oom_detection: cgroup_writable,
253                cpu_quota: false,
254                ledger: true,
255                capacity: ResourceCapacity {
256                    task_slots: 4,
257                    memory_bytes: Some(1024),
258                    pids: Some(64),
259                },
260            },
261            stable_semantics: vec![],
262            enhanced_semantics: vec![],
263            warnings: vec![],
264            degraded: false,
265            overrides: BTreeMap::new(),
266        }
267    }
268
269    fn request() -> SubmitTaskRequest {
270        SubmitTaskRequest {
271            task_id: None,
272            execution: ExecutionSpec {
273                kind: ExecutionKind::Command,
274                program: Some("/bin/echo".into()),
275                args: vec!["ok".into()],
276                script: None,
277                interpreter: None,
278                env: Default::default(),
279            },
280            limits: ResourceLimits::default(),
281            sandbox: SandboxPolicy::default(),
282            policy: None,
283            control_context: None,
284            metadata: BTreeMap::new(),
285        }
286    }
287
288    #[test]
289    fn process_request_stays_stable() {
290        let plan = resolve_execution_plan(
291            &request(),
292            &capabilities(false, false),
293            CapabilityMode::Adaptive,
294        )
295        .expect("plan");
296        assert!(!plan.degraded);
297        assert_eq!(plan.effective_sandbox.profile, SandboxProfile::Process);
298    }
299
300    #[test]
301    fn adaptive_linux_sandbox_falls_back() {
302        let mut request = request();
303        request.sandbox.profile = SandboxProfile::LinuxSandbox;
304
305        let plan = resolve_execution_plan(
306            &request,
307            &capabilities(false, false),
308            CapabilityMode::Adaptive,
309        )
310        .expect("plan");
311        assert!(plan.degraded);
312        assert_eq!(plan.effective_sandbox.profile, SandboxProfile::Process);
313    }
314
315    #[test]
316    fn strict_linux_sandbox_rejects() {
317        let mut request = request();
318        request.sandbox.profile = SandboxProfile::LinuxSandbox;
319        request.policy = Some(TaskPolicy {
320            capability_mode: CapabilityMode::Strict,
321        });
322
323        let err = resolve_execution_plan(
324            &request,
325            &capabilities(false, false),
326            CapabilityMode::Adaptive,
327        )
328        .expect_err("strict should reject");
329        assert!(matches!(err, AppError::UnsupportedCapability(_)));
330    }
331
332    #[test]
333    fn control_context_can_enforce_strict_sandbox() {
334        let mut request = request();
335        request.sandbox.profile = SandboxProfile::LinuxSandbox;
336        request.control_context = Some(ControlContext {
337            requires_strict_sandbox: true,
338            ..ControlContext::default()
339        });
340
341        let err = resolve_execution_plan(
342            &request,
343            &capabilities(false, false),
344            CapabilityMode::Adaptive,
345        )
346        .expect_err("control context should reject");
347        assert!(matches!(err, AppError::UnsupportedCapability(_)));
348    }
349}