1use crate::NativeRuntimeBackend;
2use anyhow::{Context, Result, bail};
3use serde::{Deserialize, Serialize};
4use sha2::{Digest, Sha256};
5use std::{
6 collections::BTreeMap,
7 fs::{self, File},
8 io::Read,
9 path::{Component, Path, PathBuf},
10};
11
12pub const NATIVE_RUNTIME_MANIFEST_FILE: &str = "manifest.json";
13
14#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
15pub struct NativeRuntimePlatform {
16 pub os: String,
17 pub arch: String,
18 #[serde(default, skip_serializing_if = "Option::is_none")]
19 pub target: Option<String>,
20}
21
22#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
23pub struct NativeRuntimeArtifact {
24 pub id: String,
25 #[serde(default, skip_serializing_if = "Option::is_none")]
26 pub mesh_version: Option<String>,
27 pub skippy_abi: String,
28 pub platform: NativeRuntimePlatform,
29 pub backend: NativeRuntimeBackend,
30 #[serde(default)]
31 pub rank: i64,
32 pub libraries: Vec<String>,
33 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
34 pub files: BTreeMap<String, String>,
35 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
36 pub tools: BTreeMap<String, String>,
37 #[serde(default, skip_serializing_if = "Option::is_none")]
38 pub url: Option<String>,
39 #[serde(default, skip_serializing_if = "Option::is_none")]
40 pub sha256: Option<String>,
41 #[serde(default, skip_serializing_if = "Option::is_none")]
42 pub signature: Option<String>,
43}
44
45#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
46pub struct NativeRuntimeManifest {
47 pub runtime: NativeRuntimeArtifact,
48}
49
50#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
51pub struct NativeRuntimeReleaseManifest {
52 pub mesh_version: String,
53 pub skippy_abi: String,
54 #[serde(default)]
55 pub artifacts: Vec<NativeRuntimeArtifact>,
56}
57
58impl NativeRuntimeArtifact {
59 pub fn native_runtime_id(&self) -> &str {
60 &self.id
61 }
62
63 pub fn mesh_version_or<'a>(&'a self, fallback: &'a str) -> &'a str {
64 self.mesh_version.as_deref().unwrap_or(fallback)
65 }
66}
67
68impl NativeRuntimeManifest {
69 pub fn read_from_dir(dir: &Path) -> Result<Self> {
70 let path = dir.join(NATIVE_RUNTIME_MANIFEST_FILE);
71 let text = fs::read_to_string(&path)
72 .with_context(|| format!("read native runtime manifest {}", path.display()))?;
73 let manifest: Self = serde_json::from_str(&text)
74 .with_context(|| format!("parse native runtime manifest {}", path.display()))?;
75 manifest.validate()?;
76 manifest.verify_contents(dir)?;
77 Ok(manifest)
78 }
79
80 pub fn write_to_dir(&self, dir: &Path) -> Result<()> {
81 fs::create_dir_all(dir)
82 .with_context(|| format!("create native runtime dir {}", dir.display()))?;
83 let mut manifest = self.clone();
84 if manifest.runtime.files.is_empty() {
85 for library in &manifest.runtime.libraries {
86 let path = checked_runtime_path(dir, library)?;
87 manifest
88 .runtime
89 .files
90 .insert(library.clone(), sha256_file(&path)?);
91 }
92 }
93 manifest.validate()?;
94 let path = dir.join(NATIVE_RUNTIME_MANIFEST_FILE);
95 let text = serde_json::to_string_pretty(&manifest)?;
96 fs::write(&path, format!("{text}\n"))
97 .with_context(|| format!("write native runtime manifest {}", path.display()))
98 }
99
100 pub fn validate(&self) -> Result<()> {
101 validate_artifact(&self.runtime)
102 }
103
104 fn verify_contents(&self, dir: &Path) -> Result<()> {
105 if self.runtime.files.is_empty() {
106 bail!(
107 "native runtime artifact {} does not declare file checksums",
108 self.runtime.id
109 );
110 }
111 for library in &self.runtime.libraries {
112 if !self.runtime.files.contains_key(library) {
113 bail!(
114 "native runtime artifact {} library {} is missing a file checksum",
115 self.runtime.id,
116 library
117 );
118 }
119 }
120 verify_file_checksums(dir, "file", &self.runtime.files)?;
121 verify_file_checksums(dir, "tool", &self.runtime.tools)
122 }
123}
124
125impl NativeRuntimeReleaseManifest {
126 pub fn read_from_path(path: &Path) -> Result<Self> {
127 let text = fs::read_to_string(path)
128 .with_context(|| format!("read native runtime release manifest {}", path.display()))?;
129 Self::from_json_str(&text)
130 .with_context(|| format!("parse native runtime release manifest {}", path.display()))
131 }
132
133 pub fn from_json_str(text: &str) -> Result<Self> {
134 let manifest: Self =
135 serde_json::from_str(text).context("parse native runtime release manifest")?;
136 manifest.validate()?;
137 Ok(manifest)
138 }
139
140 pub fn validate(&self) -> Result<()> {
141 if self.mesh_version.trim().is_empty() {
142 bail!("native runtime release manifest mesh_version is empty");
143 }
144 if self.skippy_abi.trim().is_empty() {
145 bail!("native runtime release manifest skippy_abi is empty");
146 }
147 for artifact in &self.artifacts {
148 validate_artifact(artifact)?;
149 if artifact.skippy_abi != self.skippy_abi {
150 bail!(
151 "native runtime artifact {} has skippy_abi {}, expected {}",
152 artifact.id,
153 artifact.skippy_abi,
154 self.skippy_abi
155 );
156 }
157 }
158 Ok(())
159 }
160}
161
162fn validate_artifact(artifact: &NativeRuntimeArtifact) -> Result<()> {
163 if artifact.id.trim().is_empty() {
164 bail!("native runtime artifact id is empty");
165 }
166 if artifact.skippy_abi.trim().is_empty() {
167 bail!(
168 "native runtime artifact {} skippy_abi is empty",
169 artifact.id
170 );
171 }
172 if artifact.platform.os.trim().is_empty() || artifact.platform.arch.trim().is_empty() {
173 bail!(
174 "native runtime artifact {} must declare platform os and arch",
175 artifact.id
176 );
177 }
178 if artifact.libraries.is_empty() {
179 bail!(
180 "native runtime artifact {} must declare at least one library",
181 artifact.id
182 );
183 }
184 for (path, checksum) in artifact.files.iter().chain(&artifact.tools) {
185 validate_runtime_path(path)?;
186 normalize_sha256(checksum).with_context(|| {
187 format!(
188 "native runtime artifact {} has invalid checksum for {}",
189 artifact.id, path
190 )
191 })?;
192 }
193 Ok(())
194}
195
196fn verify_file_checksums(
197 root: &Path,
198 kind: &str,
199 checksums: &BTreeMap<String, String>,
200) -> Result<()> {
201 for (relative, expected) in checksums {
202 let path = checked_runtime_path(root, relative)?;
203 let actual = sha256_file(&path)?;
204 let expected = normalize_sha256(expected)?;
205 if actual != expected {
206 bail!(
207 "native runtime {kind} checksum mismatch for {relative}: expected {expected}, got {actual}"
208 );
209 }
210 }
211 Ok(())
212}
213
214fn checked_runtime_path(root: &Path, relative: &str) -> Result<PathBuf> {
215 validate_runtime_path(relative)?;
216 let root = root
217 .canonicalize()
218 .with_context(|| format!("canonicalize native runtime root {}", root.display()))?;
219 let path = root.join(relative);
220 let path = path
221 .canonicalize()
222 .with_context(|| format!("canonicalize native runtime file {}", path.display()))?;
223 if !path.starts_with(&root) {
224 bail!("native runtime path escapes its bundle: {relative}");
225 }
226 if !path.is_file() {
227 bail!("native runtime path is not a file: {}", path.display());
228 }
229 Ok(path)
230}
231
232fn validate_runtime_path(relative: &str) -> Result<()> {
233 let path = Path::new(relative);
234 if relative.trim().is_empty()
235 || path.is_absolute()
236 || path
237 .components()
238 .any(|component| !matches!(component, Component::Normal(_)))
239 {
240 bail!("native runtime path must be a safe relative file path: {relative}");
241 }
242 Ok(())
243}
244
245fn normalize_sha256(value: &str) -> Result<String> {
246 let value = value
247 .trim()
248 .strip_prefix("sha256:")
249 .unwrap_or(value.trim())
250 .to_ascii_lowercase();
251 if value.len() != 64 || !value.bytes().all(|byte| byte.is_ascii_hexdigit()) {
252 bail!("expected a 64-character SHA-256 digest");
253 }
254 Ok(value)
255}
256
257fn sha256_file(path: &Path) -> Result<String> {
258 let mut file =
259 File::open(path).with_context(|| format!("open native runtime file {}", path.display()))?;
260 let mut digest = Sha256::new();
261 let mut buffer = vec![0_u8; 1024 * 1024];
266 loop {
267 let count = file
268 .read(&mut buffer)
269 .with_context(|| format!("read native runtime file {}", path.display()))?;
270 if count == 0 {
271 break;
272 }
273 digest.update(&buffer[..count]);
274 }
275 Ok(format!("{:x}", digest.finalize()))
276}
277
278#[cfg(test)]
279mod tests {
280 use super::*;
281 use crate::NativeRuntimeBackend;
282
283 #[test]
284 fn reads_native_runtime_manifest_shape() {
285 let temp = tempfile::tempdir().unwrap();
286 let library = temp.path().join("lib/libllama.so");
287 fs::create_dir_all(library.parent().unwrap()).unwrap();
288 fs::write(&library, b"native runtime").unwrap();
289 let checksum = sha256_file(&library).unwrap();
290 fs::write(
291 temp.path().join(NATIVE_RUNTIME_MANIFEST_FILE),
292 r#"{
293 "runtime": {
294 "id": "meshllm-runtime-linux-x86_64-cuda12",
295 "mesh_version": "0.68.0",
296 "skippy_abi": "0.1.25",
297 "platform": {
298 "os": "linux",
299 "arch": "x86_64",
300 "target": "x86_64-unknown-linux-gnu"
301 },
302 "backend": {
303 "kind": "cuda",
304 "cuda": {
305 "toolkit_major": 12,
306 "gpu_arches": ["sm_90"]
307 }
308 },
309 "rank": 650,
310 "libraries": ["lib/libllama.so"],
311 "files": {"lib/libllama.so": "__CHECKSUM__"},
312 "tools": {}
313 }
314}"#
315 .replace("__CHECKSUM__", &checksum),
316 )
317 .unwrap();
318
319 let manifest = NativeRuntimeManifest::read_from_dir(temp.path()).unwrap();
320
321 assert_eq!(manifest.runtime.id, "meshllm-runtime-linux-x86_64-cuda12");
322 assert_eq!(manifest.runtime.skippy_abi, "0.1.25");
323 assert_eq!(manifest.runtime.backend.kind.as_str(), "cuda");
324 }
325
326 #[test]
327 fn reads_release_manifest() {
328 let manifest = NativeRuntimeReleaseManifest::from_json_str(
329 r#"{
330 "mesh_version": "0.68.0",
331 "skippy_abi": "0.1.25",
332 "artifacts": [
333 {
334 "id": "meshllm-runtime-linux-x86_64-cpu",
335 "mesh_version": "0.68.0",
336 "skippy_abi": "0.1.25",
337 "platform": { "os": "linux", "arch": "x86_64" },
338 "backend": { "kind": "cpu" },
339 "rank": 100,
340 "libraries": ["lib/libllama.so"]
341 }
342 ]
343}"#,
344 )
345 .unwrap();
346
347 assert_eq!(manifest.artifacts.len(), 1);
348 assert_eq!(manifest.artifacts[0].backend, NativeRuntimeBackend::cpu());
349 }
350
351 #[test]
352 fn rejects_tampered_runtime_file() {
353 let temp = tempfile::tempdir().unwrap();
354 let library = temp.path().join("lib/libllama.so");
355 fs::create_dir_all(library.parent().unwrap()).unwrap();
356 fs::write(&library, b"native runtime").unwrap();
357 let manifest = NativeRuntimeManifest {
358 runtime: NativeRuntimeArtifact {
359 id: "meshllm-runtime-linux-x86_64-cpu".to_string(),
360 mesh_version: Some("0.68.0".to_string()),
361 skippy_abi: "0.1.25".to_string(),
362 platform: NativeRuntimePlatform {
363 os: "linux".to_string(),
364 arch: "x86_64".to_string(),
365 target: None,
366 },
367 backend: NativeRuntimeBackend::cpu(),
368 rank: 0,
369 libraries: vec!["lib/libllama.so".to_string()],
370 files: Default::default(),
371 tools: Default::default(),
372 url: None,
373 sha256: None,
374 signature: None,
375 },
376 };
377 manifest.write_to_dir(temp.path()).unwrap();
378 fs::write(&library, b"tampered runtime").unwrap();
379
380 let error = NativeRuntimeManifest::read_from_dir(temp.path()).unwrap_err();
381
382 assert!(error.to_string().contains("checksum mismatch"));
383 assert!(error.to_string().contains("lib/libllama.so"));
384 }
385
386 #[test]
387 fn rejects_tampered_runtime_tool() {
388 let temp = tempfile::tempdir().unwrap();
389 let library = temp.path().join("lib/libllama.so");
390 let tool = temp.path().join("tools/mesh-llm-gpu-benchmark");
391 fs::create_dir_all(library.parent().unwrap()).unwrap();
392 fs::create_dir_all(tool.parent().unwrap()).unwrap();
393 fs::write(&library, b"native runtime").unwrap();
394 fs::write(&tool, b"benchmark tool").unwrap();
395 let manifest = NativeRuntimeManifest {
396 runtime: NativeRuntimeArtifact {
397 id: "meshllm-runtime-linux-x86_64-cuda12".to_string(),
398 mesh_version: Some("0.68.0".to_string()),
399 skippy_abi: "0.1.25".to_string(),
400 platform: NativeRuntimePlatform {
401 os: "linux".to_string(),
402 arch: "x86_64".to_string(),
403 target: None,
404 },
405 backend: NativeRuntimeBackend::cuda(12, vec!["sm_90".to_string()]),
406 rank: 0,
407 libraries: vec!["lib/libllama.so".to_string()],
408 files: BTreeMap::from([(
409 "lib/libllama.so".to_string(),
410 sha256_file(&library).unwrap(),
411 )]),
412 tools: BTreeMap::from([(
413 "tools/mesh-llm-gpu-benchmark".to_string(),
414 sha256_file(&tool).unwrap(),
415 )]),
416 url: None,
417 sha256: None,
418 signature: None,
419 },
420 };
421 manifest.write_to_dir(temp.path()).unwrap();
422 fs::write(&tool, b"tampered benchmark tool").unwrap();
423
424 let error = NativeRuntimeManifest::read_from_dir(temp.path()).unwrap_err();
425
426 assert!(error.to_string().contains("checksum mismatch"));
427 assert!(error.to_string().contains("tools/mesh-llm-gpu-benchmark"));
428 }
429
430 #[test]
431 fn checksum_verification_runs_on_a_small_foreign_thread_stack() {
432 let file = tempfile::NamedTempFile::new().unwrap();
433 fs::write(file.path(), b"native runtime").unwrap();
434 let path = file.path().to_path_buf();
435
436 let checksum = std::thread::Builder::new()
437 .name("native-runtime-checksum-test".to_string())
438 .stack_size(256 * 1024)
439 .spawn(move || sha256_file(&path))
440 .unwrap()
441 .join()
442 .unwrap()
443 .unwrap();
444
445 assert_eq!(checksum, sha256_file(file.path()).unwrap());
446 }
447
448 #[test]
449 fn rejects_runtime_manifest_without_file_checksums() {
450 let temp = tempfile::tempdir().unwrap();
451 fs::create_dir_all(temp.path().join("lib")).unwrap();
452 fs::write(temp.path().join("lib/libllama.so"), b"native runtime").unwrap();
453 fs::write(
454 temp.path().join(NATIVE_RUNTIME_MANIFEST_FILE),
455 r#"{
456 "runtime": {
457 "id": "meshllm-runtime-linux-x86_64-cpu",
458 "mesh_version": "0.68.0",
459 "skippy_abi": "0.1.25",
460 "platform": {"os": "linux", "arch": "x86_64"},
461 "backend": {"kind": "cpu"},
462 "libraries": ["lib/libllama.so"]
463 }
464}"#,
465 )
466 .unwrap();
467
468 let error = NativeRuntimeManifest::read_from_dir(temp.path()).unwrap_err();
469
470 assert!(
471 error
472 .to_string()
473 .contains("does not declare file checksums")
474 );
475 }
476
477 #[test]
478 fn rejects_runtime_checksum_path_traversal() {
479 let artifact = NativeRuntimeArtifact {
480 id: "meshllm-runtime-linux-x86_64-cpu".to_string(),
481 mesh_version: Some("0.68.0".to_string()),
482 skippy_abi: "0.1.25".to_string(),
483 platform: NativeRuntimePlatform {
484 os: "linux".to_string(),
485 arch: "x86_64".to_string(),
486 target: None,
487 },
488 backend: NativeRuntimeBackend::cpu(),
489 rank: 0,
490 libraries: vec!["lib/libllama.so".to_string()],
491 files: BTreeMap::from([("../outside".to_string(), "0".repeat(64))]),
492 tools: Default::default(),
493 url: None,
494 sha256: None,
495 signature: None,
496 };
497
498 let error = validate_artifact(&artifact).unwrap_err();
499
500 assert!(error.to_string().contains("safe relative file path"));
501 }
502}