nucleus/checkpoint/
metadata.rs1use crate::container::{ContainerState, ContainerStateManager};
2use crate::error::{NucleusError, Result};
3use serde::{Deserialize, Serialize};
4use std::fs;
5use std::fs::OpenOptions;
6use std::io::Write;
7use std::os::unix::fs::OpenOptionsExt;
8use std::path::Path;
9use std::time::SystemTime;
10
11#[derive(Debug, Clone, Serialize, Deserialize)]
13pub struct CheckpointMetadata {
14 pub container_id: String,
16
17 pub container_name: String,
19
20 pub original_pid: u32,
22
23 pub command: Vec<String>,
25
26 pub checkpoint_at: u64,
28
29 pub version: String,
31
32 pub using_gvisor: bool,
34
35 pub rootless: bool,
37}
38
39impl CheckpointMetadata {
40 pub fn from_state(state: &ContainerState) -> Self {
42 let checkpoint_at = SystemTime::now()
43 .duration_since(SystemTime::UNIX_EPOCH)
44 .unwrap_or_default()
45 .as_secs();
46
47 Self {
48 container_id: state.id.clone(),
49 container_name: state.name.clone(),
50 original_pid: state.pid,
51 command: state.command.clone(),
52 checkpoint_at,
53 version: env!("CARGO_PKG_VERSION").to_string(),
54 using_gvisor: state.using_gvisor,
55 rootless: state.rootless,
56 }
57 }
58
59 pub fn save(&self, dir: &Path) -> Result<()> {
61 let path = dir.join("metadata.json");
62 let tmp_path = dir.join("metadata.json.tmp");
63 let json = serde_json::to_string_pretty(self).map_err(|e| {
64 NucleusError::CheckpointError(format!("Failed to serialize metadata: {}", e))
65 })?;
66
67 if tmp_path.exists() {
68 let meta = fs::symlink_metadata(&tmp_path).map_err(|e| {
69 NucleusError::CheckpointError(format!(
70 "Failed to inspect temp metadata file {:?}: {}",
71 tmp_path, e
72 ))
73 })?;
74 if meta.file_type().is_symlink() {
75 return Err(NucleusError::CheckpointError(format!(
76 "Refusing symlink temp metadata file {:?}",
77 tmp_path
78 )));
79 }
80 fs::remove_file(&tmp_path).map_err(|e| {
81 NucleusError::CheckpointError(format!(
82 "Failed to remove stale temp metadata file {:?}: {}",
83 tmp_path, e
84 ))
85 })?;
86 }
87
88 let mut file = OpenOptions::new()
89 .create_new(true)
90 .write(true)
91 .mode(0o600)
92 .custom_flags(libc::O_NOFOLLOW)
93 .open(&tmp_path)
94 .map_err(|e| {
95 NucleusError::CheckpointError(format!(
96 "Failed to open temp metadata file {:?}: {}",
97 tmp_path, e
98 ))
99 })?;
100
101 file.write_all(json.as_bytes()).map_err(|e| {
102 NucleusError::CheckpointError(format!(
103 "Failed to write metadata file {:?}: {}",
104 tmp_path, e
105 ))
106 })?;
107 file.sync_all().map_err(|e| {
108 NucleusError::CheckpointError(format!(
109 "Failed to sync metadata file {:?}: {}",
110 tmp_path, e
111 ))
112 })?;
113
114 fs::rename(&tmp_path, &path).map_err(|e| {
115 NucleusError::CheckpointError(format!(
116 "Failed to atomically replace metadata file {:?}: {}",
117 path, e
118 ))
119 })?;
120 Ok(())
121 }
122
123 pub fn load(dir: &Path) -> Result<Self> {
125 let path = dir.join("metadata.json");
126 let json = ContainerStateManager::read_file_nofollow(&path).map_err(|e| {
127 NucleusError::CheckpointError(format!("Failed to read metadata {:?}: {}", path, e))
128 })?;
129 let metadata: Self = serde_json::from_str(&json).map_err(|e| {
130 NucleusError::CheckpointError(format!("Failed to parse metadata: {}", e))
131 })?;
132 Ok(metadata)
133 }
134}
135
136#[cfg(test)]
137mod tests {
138 use super::*;
139 use std::os::unix::fs as unix_fs;
140
141 #[test]
142 fn test_save_rejects_symlink_target() {
143 let dir = tempfile::tempdir().unwrap();
147 let attacker_target = dir.path().join("attacker-owned-file");
148 std::fs::write(&attacker_target, "").unwrap();
149
150 let symlink_path = dir.path().join("metadata.json.tmp");
152 unix_fs::symlink(&attacker_target, &symlink_path).unwrap();
153
154 let metadata = CheckpointMetadata {
155 container_id: "test-id".to_string(),
156 container_name: "test".to_string(),
157 original_pid: 1,
158 command: vec!["/bin/sh".to_string()],
159 checkpoint_at: 0,
160 version: "0.0.0".to_string(),
161 using_gvisor: false,
162 rootless: false,
163 };
164
165 let result = metadata.save(dir.path());
166 assert!(
167 result.is_err(),
168 "save() must reject symlink at temp file path (O_NOFOLLOW / symlink check)"
169 );
170 }
171}