Skip to main content

microsandbox_types/snapshot/
owned.rs

1//! Required snapshot inventory for storage whose lifetime belongs to one sandbox.
2
3use std::collections::BTreeSet;
4use std::path::{Path, PathBuf};
5
6use crate::{HostPermissions, MountOptions, OwnedVolumeStorage, StatVirtualization, VolumeMount};
7use serde::{Deserialize, Serialize};
8
9use super::Manifest;
10use super::disk::DiskGenerationManifest;
11use crate::error::{SnapshotManifestError, SnapshotManifestResult};
12
13//--------------------------------------------------------------------------------------------------
14// Constants
15//--------------------------------------------------------------------------------------------------
16
17/// Must-understand extension for complete, privately restored sandbox-owned storage.
18pub const OWNED_VOLUMES_EXTENSION: &str = "microsandbox.owned-volumes";
19
20//--------------------------------------------------------------------------------------------------
21// Types
22//--------------------------------------------------------------------------------------------------
23
24/// One immutable regular payload, addressed by its exact SHA-256 bytes.
25#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
26#[serde(deny_unknown_fields)]
27pub struct OwnedDirectoryPayload {
28    /// Lowercase unqualified SHA-256 digest.
29    pub digest: String,
30    /// Exact logical length, including sparse holes.
31    pub bytes: u64,
32}
33
34/// Complete captured backing for one owned mount.
35#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
36#[serde(tag = "kind", rename_all = "lowercase", deny_unknown_fields)]
37pub enum OwnedVolumeData {
38    /// A complete immutable disk chain using the existing layer store.
39    Disk {
40        /// Captured bytes and their guest-visible block identity.
41        generation: DiskGenerationManifest,
42    },
43    /// A namespace descriptor and all linked or detached regular-file payloads.
44    Directory {
45        /// Identity of `owned/<mount_id>/directory.bin`.
46        descriptor: OwnedDirectoryPayload,
47        /// Sorted unique content payloads under `owned/<mount_id>/files/`.
48        files: Vec<OwnedDirectoryPayload>,
49    },
50}
51
52/// Required ownership and content inventory; never contains a caller-selected host path.
53#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
54#[serde(deny_unknown_fields)]
55pub struct OwnedVolumeCapture {
56    /// Stable mount tag derived by the launcher from the canonical guest path.
57    pub mount_id: String,
58    /// Lossless owned mount configuration, including guest metadata and mount policies.
59    pub mount: OwnedMountSnapshot,
60    /// Complete private backing required before child activation.
61    pub data: OwnedVolumeData,
62}
63
64/// Owned-only mount metadata; its type cannot encode an external host binding.
65#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
66#[serde(deny_unknown_fields)]
67pub struct OwnedMountSnapshot {
68    /// Canonical guest destination.
69    pub guest: String,
70    /// Directory quota or disk capacity.
71    pub storage: OwnedVolumeStorage,
72    /// Common mount flags and owner policy.
73    pub options: MountOptions,
74    /// Directory stat virtualization policy.
75    pub stat_virtualization: StatVirtualization,
76    /// Directory host-permission policy.
77    pub host_permissions: HostPermissions,
78}
79
80//--------------------------------------------------------------------------------------------------
81// Methods
82//--------------------------------------------------------------------------------------------------
83
84impl Manifest {
85    /// Record required owned state without changing the released root-layer semantics.
86    pub fn set_owned_volumes(
87        &mut self,
88        volumes: Vec<OwnedVolumeCapture>,
89    ) -> SnapshotManifestResult<()> {
90        validate_owned_volumes(&volumes)?;
91        self.requires.retain(|key| key != OWNED_VOLUMES_EXTENSION);
92        if volumes.is_empty() {
93            self.extensions.remove(OWNED_VOLUMES_EXTENSION);
94        } else {
95            self.extensions.insert(
96                OWNED_VOLUMES_EXTENSION.into(),
97                serde_json::to_value(volumes)
98                    .map_err(|error| SnapshotManifestError::ManifestParse(error.to_string()))?,
99            );
100            self.requires.push(OWNED_VOLUMES_EXTENSION.into());
101            self.requires.sort();
102        }
103        Ok(())
104    }
105
106    /// Read and validate the required inventory; released snapshots have no owned mounts.
107    pub fn owned_volumes(&self) -> SnapshotManifestResult<Vec<OwnedVolumeCapture>> {
108        let Some(value) = self.extensions.get(OWNED_VOLUMES_EXTENSION) else {
109            return Ok(Vec::new());
110        };
111        if !self
112            .requires
113            .iter()
114            .any(|key| key == OWNED_VOLUMES_EXTENSION)
115        {
116            return invalid("owned volumes must be a required snapshot extension");
117        }
118        let volumes: Vec<OwnedVolumeCapture> =
119            serde_json::from_value(value.clone()).map_err(|error| {
120                SnapshotManifestError::ManifestParse(format!("owned volumes: {error}"))
121            })?;
122        validate_owned_volumes(&volumes)?;
123        Ok(volumes)
124    }
125}
126
127impl OwnedVolumeCapture {
128    /// Relative directory containing this mount's immutable filesystem generation.
129    pub fn directory_path(&self) -> PathBuf {
130        Path::new("owned").join(&self.mount_id)
131    }
132
133    /// Enumerate only required directory files; disk layers use the existing disk inventory.
134    pub fn directory_payloads(&self) -> Vec<(PathBuf, &OwnedDirectoryPayload)> {
135        match &self.data {
136            OwnedVolumeData::Disk { .. } => Vec::new(),
137            OwnedVolumeData::Directory { descriptor, files } => {
138                let directory = self.directory_path();
139                std::iter::once((directory.join("directory.bin"), descriptor))
140                    .chain(
141                        files
142                            .iter()
143                            .map(|file| (directory.join("files").join(&file.digest), file)),
144                    )
145                    .collect()
146            }
147        }
148    }
149}
150
151impl OwnedMountSnapshot {
152    /// Extract only sandbox-owned configuration from a public mount value.
153    pub fn from_mount(mount: &VolumeMount) -> SnapshotManifestResult<Self> {
154        let VolumeMount::Owned {
155            guest,
156            storage,
157            options,
158            stat_virtualization,
159            host_permissions,
160        } = mount
161        else {
162            return invalid("owned volume inventory contains an external mount");
163        };
164        Ok(Self {
165            guest: guest.clone(),
166            storage: storage.clone(),
167            options: *options,
168            stat_virtualization: *stat_virtualization,
169            host_permissions: *host_permissions,
170        })
171    }
172
173    /// Reconstruct the lossless public owned discriminant after private materialization.
174    pub fn to_mount(&self) -> VolumeMount {
175        VolumeMount::Owned {
176            guest: self.guest.clone(),
177            storage: self.storage.clone(),
178            options: self.options,
179            stat_virtualization: self.stat_virtualization,
180            host_permissions: self.host_permissions,
181        }
182    }
183}
184
185//--------------------------------------------------------------------------------------------------
186// Functions
187//--------------------------------------------------------------------------------------------------
188
189/// Validate confined identities and the agreement between ownership and captured backing.
190pub fn validate_owned_volumes(volumes: &[OwnedVolumeCapture]) -> SnapshotManifestResult<()> {
191    if volumes.len() > 256 {
192        return invalid("owned volume count exceeds the format bound");
193    }
194    let mut ids = BTreeSet::new();
195    let mut guests = BTreeSet::new();
196    for volume in volumes {
197        if volume.mount_id.is_empty()
198            || volume.mount_id.len() > 128
199            || !volume
200                .mount_id
201                .bytes()
202                .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'_' | b'-'))
203            || !ids.insert(&volume.mount_id)
204            || !guests.insert(volume.mount.guest.as_str())
205        {
206            return invalid("owned volume has an invalid or repeated mount identity");
207        }
208        let guest = &volume.mount.guest;
209        let storage = &volume.mount.storage;
210        if crate::owned_volume_mount_id(guest) != volume.mount_id {
211            return invalid("owned volume mount identity differs from its guest path");
212        }
213        if guest == "/"
214            || !guest.starts_with('/')
215            || guest
216                .split('/')
217                .skip(1)
218                .any(|part| matches!(part, "" | "." | ".."))
219        {
220            return invalid("owned volume guest path is not canonical");
221        }
222        match (storage, &volume.data) {
223            (OwnedVolumeStorage::Disk { capacity_mib }, OwnedVolumeData::Disk { generation }) => {
224                generation.validate()?;
225                if *capacity_mib == 0
226                    || generation.device_id != volume.mount_id
227                    || generation
228                        .layers
229                        .iter()
230                        .any(|layer| layer.virtual_size != u64::from(*capacity_mib) * 1024 * 1024)
231                {
232                    return invalid("owned disk generation differs from its storage specification");
233                }
234            }
235            (
236                OwnedVolumeStorage::Directory { .. },
237                OwnedVolumeData::Directory { descriptor, files },
238            ) => {
239                validate_payload(descriptor)?;
240                let mut previous = None;
241                for payload in files {
242                    validate_payload(payload)?;
243                    if previous.is_some_and(|digest: &str| digest >= payload.digest.as_str()) {
244                        return invalid("owned directory payloads must be sorted and unique");
245                    }
246                    previous = Some(payload.digest.as_str());
247                }
248            }
249            _ => return invalid("owned volume storage and captured backing kinds differ"),
250        }
251    }
252    Ok(())
253}
254
255fn validate_payload(payload: &OwnedDirectoryPayload) -> SnapshotManifestResult<()> {
256    if payload.digest.len() != 64
257        || !payload
258            .digest
259            .bytes()
260            .all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte))
261    {
262        return invalid("owned directory payload has an invalid SHA-256 identity");
263    }
264    Ok(())
265}
266
267fn invalid<T>(message: &str) -> SnapshotManifestResult<T> {
268    Err(SnapshotManifestError::ManifestParse(message.into()))
269}