Skip to main content

mesh_llm_native_runtime/
manifest.rs

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    // This is reached through the Kotlin/UniFFI SDK while running on a JVM
262    // native thread. JVM-created native threads can have a 1 MiB stack, so a
263    // 1 MiB local array can overflow that stack before checksum verification
264    // even begins. Keep the read buffer on the heap instead.
265    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}