⚠️ VeridianOS Kernel Documentation - This is low-level kernel code. All functions are unsafe unless explicitly marked otherwise. no_std

veridian_kernel/virt/containers/
oci.rs

1//! OCI Runtime Specification - container lifecycle, config parsing, hooks,
2//! pivot_root.
3
4#[cfg(feature = "alloc")]
5use alloc::{string::String, vec::Vec};
6use core::sync::atomic::{AtomicU64, Ordering};
7
8use super::{parse_u32, parse_u64};
9use crate::error::KernelError;
10
11/// OCI container lifecycle states per the runtime-spec.
12#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13pub enum OciLifecycleState {
14    /// Container bundle loaded, namespaces created, but process not started.
15    Creating,
16    /// Container created but user process not yet started (post-create hooks
17    /// ran).
18    Created,
19    /// Container process is running.
20    Running,
21    /// Container process has exited.
22    Stopped,
23}
24
25impl core::fmt::Display for OciLifecycleState {
26    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
27        match self {
28            Self::Creating => write!(f, "creating"),
29            Self::Created => write!(f, "created"),
30            Self::Running => write!(f, "running"),
31            Self::Stopped => write!(f, "stopped"),
32        }
33    }
34}
35
36/// A single mount specification from the OCI config.
37#[cfg(feature = "alloc")]
38#[derive(Debug, Clone, PartialEq, Eq)]
39pub struct OciMount {
40    /// Destination path inside the container.
41    pub destination: String,
42    /// Mount type (e.g., "proc", "tmpfs", "bind").
43    pub mount_type: String,
44    /// Source path on the host.
45    pub source: String,
46    /// Mount options (e.g., "nosuid", "noexec", "ro").
47    pub options: Vec<String>,
48}
49
50/// Linux namespace configuration from the OCI config.
51#[derive(Debug, Clone, Copy, PartialEq, Eq)]
52pub enum OciNamespaceKind {
53    Pid,
54    Network,
55    Mount,
56    Ipc,
57    Uts,
58    User,
59    Cgroup,
60}
61
62impl OciNamespaceKind {
63    /// Parse from OCI config string.
64    pub fn from_str_kind(s: &str) -> Option<Self> {
65        match s {
66            "pid" => Some(Self::Pid),
67            "network" => Some(Self::Network),
68            "mount" => Some(Self::Mount),
69            "ipc" => Some(Self::Ipc),
70            "uts" => Some(Self::Uts),
71            "user" => Some(Self::User),
72            "cgroup" => Some(Self::Cgroup),
73            _ => None,
74        }
75    }
76
77    /// Return the OCI string representation.
78    pub fn as_str(&self) -> &'static str {
79        match self {
80            Self::Pid => "pid",
81            Self::Network => "network",
82            Self::Mount => "mount",
83            Self::Ipc => "ipc",
84            Self::Uts => "uts",
85            Self::User => "user",
86            Self::Cgroup => "cgroup",
87        }
88    }
89}
90
91/// A namespace entry in the OCI linux config.
92#[cfg(feature = "alloc")]
93#[derive(Debug, Clone, PartialEq, Eq)]
94pub struct OciNamespace {
95    pub kind: OciNamespaceKind,
96    /// Optional path to an existing namespace to join.
97    pub path: Option<String>,
98}
99
100/// Lifecycle hook specification.
101#[cfg(feature = "alloc")]
102#[derive(Debug, Clone, PartialEq, Eq)]
103pub struct OciHook {
104    /// Path to the hook executable.
105    pub path: String,
106    /// Arguments passed to the hook.
107    pub args: Vec<String>,
108    /// Environment variables for the hook.
109    pub env: Vec<String>,
110    /// Timeout in seconds (0 = no timeout).
111    pub timeout_secs: u32,
112}
113
114/// OCI hooks at different lifecycle points.
115#[cfg(feature = "alloc")]
116#[derive(Debug, Clone, Default, PartialEq, Eq)]
117pub struct OciHooks {
118    pub prestart: Vec<OciHook>,
119    pub poststart: Vec<OciHook>,
120    pub poststop: Vec<OciHook>,
121}
122
123/// Root filesystem specification.
124#[cfg(feature = "alloc")]
125#[derive(Debug, Clone, PartialEq, Eq)]
126pub struct OciRoot {
127    /// Path to the root filesystem.
128    pub path: String,
129    /// Whether the root filesystem is read-only.
130    pub readonly: bool,
131}
132
133/// Process specification from config.json.
134#[cfg(feature = "alloc")]
135#[derive(Debug, Clone, PartialEq, Eq)]
136pub struct OciProcess {
137    /// Path to the executable.
138    pub args: Vec<String>,
139    /// Environment variables in KEY=VALUE format.
140    pub env: Vec<String>,
141    /// Working directory inside the container.
142    pub cwd: String,
143    /// User ID.
144    pub uid: u32,
145    /// Group ID.
146    pub gid: u32,
147    /// Whether a terminal is attached.
148    pub terminal: bool,
149}
150
151/// Linux-specific configuration.
152#[cfg(feature = "alloc")]
153#[derive(Debug, Clone, PartialEq, Eq)]
154pub struct OciLinuxConfig {
155    /// Namespaces to create or join.
156    pub namespaces: Vec<OciNamespace>,
157    /// Cgroups path.
158    pub cgroups_path: String,
159    /// Memory limit in bytes (0 = unlimited).
160    pub memory_limit: u64,
161    /// CPU shares (default 1024).
162    pub cpu_shares: u32,
163    /// CPU quota in microseconds per period (0 = unlimited).
164    pub cpu_quota: u64,
165    /// CPU period in microseconds (default 100000).
166    pub cpu_period: u64,
167}
168
169#[cfg(feature = "alloc")]
170impl Default for OciLinuxConfig {
171    fn default() -> Self {
172        Self {
173            namespaces: Vec::new(),
174            cgroups_path: String::new(),
175            memory_limit: 0,
176            cpu_shares: 1024,
177            cpu_quota: 0,
178            cpu_period: 100_000,
179        }
180    }
181}
182
183/// Parsed OCI runtime configuration (config.json equivalent).
184#[cfg(feature = "alloc")]
185#[derive(Debug, Clone)]
186pub struct OciConfig {
187    /// OCI specification version.
188    pub oci_version: String,
189    /// Root filesystem.
190    pub root: OciRoot,
191    /// Container process.
192    pub process: OciProcess,
193    /// Mount points.
194    pub mounts: Vec<OciMount>,
195    /// Hostname.
196    pub hostname: String,
197    /// Lifecycle hooks.
198    pub hooks: OciHooks,
199    /// Linux-specific configuration.
200    pub linux: OciLinuxConfig,
201}
202
203#[cfg(feature = "alloc")]
204impl OciConfig {
205    /// Parse a simplified config.json representation from key-value lines.
206    ///
207    /// Format: one "key=value" per line. Recognized keys:
208    ///   oci_version, root_path, root_readonly, hostname,
209    ///   process_cwd, process_uid, process_gid, process_terminal,
210    ///   process_arg, process_env,
211    ///   mount (destination:type:source:options),
212    ///   namespace (kind[:path]),
213    ///   cgroups_path, memory_limit, cpu_shares, cpu_quota, cpu_period,
214    ///   hook_prestart, hook_poststart, hook_poststop (path:timeout)
215    pub fn parse(input: &str) -> Result<Self, KernelError> {
216        let mut config = Self {
217            oci_version: String::from("1.0.2"),
218            root: OciRoot {
219                path: String::from("/"),
220                readonly: false,
221            },
222            process: OciProcess {
223                args: Vec::new(),
224                env: Vec::new(),
225                cwd: String::from("/"),
226                uid: 0,
227                gid: 0,
228                terminal: false,
229            },
230            mounts: Vec::new(),
231            hostname: String::new(),
232            hooks: OciHooks::default(),
233            linux: OciLinuxConfig::default(),
234        };
235
236        for line in input.lines() {
237            let line = line.trim();
238            if line.is_empty() || line.starts_with('#') {
239                continue;
240            }
241            if let Some((key, val)) = line.split_once('=') {
242                let key = key.trim();
243                let val = val.trim();
244                match key {
245                    "oci_version" => config.oci_version = String::from(val),
246                    "root_path" => config.root.path = String::from(val),
247                    "root_readonly" => config.root.readonly = val == "true",
248                    "hostname" => config.hostname = String::from(val),
249                    "process_cwd" => config.process.cwd = String::from(val),
250                    "process_uid" => {
251                        config.process.uid = parse_u32(val).unwrap_or(0);
252                    }
253                    "process_gid" => {
254                        config.process.gid = parse_u32(val).unwrap_or(0);
255                    }
256                    "process_terminal" => config.process.terminal = val == "true",
257                    "process_arg" => config.process.args.push(String::from(val)),
258                    "process_env" => config.process.env.push(String::from(val)),
259                    "mount" => {
260                        // destination:type:source:options
261                        let parts: Vec<&str> = val.splitn(4, ':').collect();
262                        if parts.len() >= 3 {
263                            let options = if parts.len() > 3 {
264                                parts[3]
265                                    .split(',')
266                                    .map(|s| String::from(s.trim()))
267                                    .collect()
268                            } else {
269                                Vec::new()
270                            };
271                            config.mounts.push(OciMount {
272                                destination: String::from(parts[0]),
273                                mount_type: String::from(parts[1]),
274                                source: String::from(parts[2]),
275                                options,
276                            });
277                        }
278                    }
279                    "namespace" => {
280                        if let Some((kind_str, path)) = val.split_once(':') {
281                            if let Some(kind) = OciNamespaceKind::from_str_kind(kind_str) {
282                                config.linux.namespaces.push(OciNamespace {
283                                    kind,
284                                    path: Some(String::from(path)),
285                                });
286                            }
287                        } else if let Some(kind) = OciNamespaceKind::from_str_kind(val) {
288                            config
289                                .linux
290                                .namespaces
291                                .push(OciNamespace { kind, path: None });
292                        }
293                    }
294                    "cgroups_path" => config.linux.cgroups_path = String::from(val),
295                    "memory_limit" => {
296                        config.linux.memory_limit = parse_u64(val).unwrap_or(0);
297                    }
298                    "cpu_shares" => {
299                        config.linux.cpu_shares = parse_u32(val).unwrap_or(1024);
300                    }
301                    "cpu_quota" => {
302                        config.linux.cpu_quota = parse_u64(val).unwrap_or(0);
303                    }
304                    "cpu_period" => {
305                        config.linux.cpu_period = parse_u64(val).unwrap_or(100_000);
306                    }
307                    "hook_prestart" | "hook_poststart" | "hook_poststop" => {
308                        let hook = parse_hook(val);
309                        match key {
310                            "hook_prestart" => config.hooks.prestart.push(hook),
311                            "hook_poststart" => config.hooks.poststart.push(hook),
312                            "hook_poststop" => config.hooks.poststop.push(hook),
313                            _ => {}
314                        }
315                    }
316                    _ => {} // ignore unknown keys
317                }
318            }
319        }
320
321        Ok(config)
322    }
323
324    /// Validate the configuration.
325    pub fn validate(&self) -> Result<(), KernelError> {
326        if self.root.path.is_empty() {
327            return Err(KernelError::InvalidArgument {
328                name: "root.path",
329                value: "empty",
330            });
331        }
332        if self.process.args.is_empty() {
333            return Err(KernelError::InvalidArgument {
334                name: "process.args",
335                value: "empty",
336            });
337        }
338        if self.process.cwd.is_empty() || !self.process.cwd.starts_with('/') {
339            return Err(KernelError::InvalidArgument {
340                name: "process.cwd",
341                value: "must be absolute path",
342            });
343        }
344        Ok(())
345    }
346}
347
348/// An OCI-compliant container runtime instance.
349#[cfg(feature = "alloc")]
350#[derive(Debug)]
351pub struct OciContainer {
352    /// Unique container ID.
353    pub id: String,
354    /// Current lifecycle state.
355    pub state: OciLifecycleState,
356    /// Parsed OCI configuration.
357    pub config: OciConfig,
358    /// PID of the container init process (0 if not started).
359    pub pid: u64,
360    /// Bundle path (directory containing config.json + rootfs).
361    pub bundle: String,
362    /// Creation timestamp (monotonic counter value).
363    pub created_at: u64,
364}
365
366#[cfg(feature = "alloc")]
367impl OciContainer {
368    /// Create a new container from a parsed config.
369    pub fn new(id: &str, bundle: &str, config: OciConfig) -> Result<Self, KernelError> {
370        config.validate()?;
371        Ok(Self {
372            id: String::from(id),
373            state: OciLifecycleState::Creating,
374            config,
375            pid: 0,
376            bundle: String::from(bundle),
377            created_at: CONTAINER_COUNTER.fetch_add(1, Ordering::Relaxed),
378        })
379    }
380
381    /// Transition to Created state (namespaces set up, hooks run).
382    pub fn mark_created(&mut self) -> Result<(), KernelError> {
383        if self.state != OciLifecycleState::Creating {
384            return Err(KernelError::InvalidState {
385                expected: "creating",
386                actual: self.state_str(),
387            });
388        }
389        self.state = OciLifecycleState::Created;
390        Ok(())
391    }
392
393    /// Start the container process, transitioning to Running.
394    pub fn start(&mut self, pid: u64) -> Result<(), KernelError> {
395        if self.state != OciLifecycleState::Created {
396            return Err(KernelError::InvalidState {
397                expected: "created",
398                actual: self.state_str(),
399            });
400        }
401        self.pid = pid;
402        self.state = OciLifecycleState::Running;
403        Ok(())
404    }
405
406    /// Transition to Stopped state.
407    pub fn stop(&mut self) -> Result<(), KernelError> {
408        if self.state != OciLifecycleState::Running {
409            return Err(KernelError::InvalidState {
410                expected: "running",
411                actual: self.state_str(),
412            });
413        }
414        self.state = OciLifecycleState::Stopped;
415        Ok(())
416    }
417
418    fn state_str(&self) -> &'static str {
419        match self.state {
420            OciLifecycleState::Creating => "creating",
421            OciLifecycleState::Created => "created",
422            OciLifecycleState::Running => "running",
423            OciLifecycleState::Stopped => "stopped",
424        }
425    }
426
427    /// Perform pivot_root: change the root filesystem for the container.
428    /// Returns the old root path and new root path.
429    pub fn pivot_root(&self) -> Result<(String, String), KernelError> {
430        if self.config.root.path.is_empty() {
431            return Err(KernelError::InvalidArgument {
432                name: "root",
433                value: "empty path",
434            });
435        }
436        let old_root = String::from("/.pivot_root");
437        let new_root = self.config.root.path.clone();
438        Ok((old_root, new_root))
439    }
440}
441
442static CONTAINER_COUNTER: AtomicU64 = AtomicU64::new(1);
443
444/// Parse a hook specification: "path:timeout" or just "path".
445#[cfg(feature = "alloc")]
446pub(super) fn parse_hook(val: &str) -> OciHook {
447    let (path, timeout) = if let Some((p, t)) = val.split_once(':') {
448        (p, parse_u32(t).unwrap_or(0))
449    } else {
450        (val, 0)
451    };
452    OciHook {
453        path: String::from(path),
454        args: Vec::new(),
455        env: Vec::new(),
456        timeout_secs: timeout,
457    }
458}