Skip to main content

gatehouse_runtime_protocol/lifecycle/journal/storage/slot/
mapping.rs

1//! Fixed resource mapping below one protected Core state root.
2use super::*;
3use sha2::{Digest, Sha256};
4fn name(project: &str, resource: &str) -> Result<String> {
5    crate::nonempty(project)?;
6    crate::nonempty(resource)?;
7    let mut hash = Sha256::new();
8    for value in ["gatehouse-lifecycle-resource/v1", project, resource] {
9        hash.update((value.len() as u64).to_be_bytes());
10        hash.update(value.as_bytes());
11    }
12    Ok(format!("resource-{:x}", hash.finalize()))
13}
14impl ServiceSlot {
15    /// Stable mapping is independent of image, host path, invocation or config
16    /// changes: those changes cannot select a new slot for an unresolved resource.
17    pub fn mapped_root(base: &Path, project: &str, resource: &str) -> Result<PathBuf> {
18        Ok(base.join(name(project, resource)?))
19    }
20    pub fn acquire_mapped(
21        base: &Path,
22        resolve: Request,
23        plan: CreatePlan,
24        healthcheck: Vec<String>,
25    ) -> Result<Self> {
26        resolve.validate()?;
27        let parent = Journal::directory(base)?;
28        let name = name(&resolve.binding.project_id, &resolve.binding.resource_id)?;
29        let child = CString::new(name.clone()).unwrap();
30        // SAFETY: bounded digest name and held directory descriptor. Never chmod
31        // an existing entry; Journal::directory validates its exact protection.
32        if unsafe { libc::mkdirat(parent.as_raw_fd(), child.as_ptr(), 0o700) } != 0
33            && std::io::Error::last_os_error().kind() != std::io::ErrorKind::AlreadyExists
34        {
35            return Err(Invalid("cannot provision lifecycle resource root"));
36        }
37        io(parent.sync_all())?;
38        let root = base.join(name);
39        let slot = Self::acquire(&root, resolve, plan, healthcheck)?;
40        slot.check()?;
41        Ok(slot)
42    }
43}
44
45impl ServiceSlot {
46    /// Fixed Core namespace below the canonical project root. No environment or
47    /// host/config path selects the base. Existing unsafe entries are never fixed.
48    pub fn project_state_base(project: &Path) -> Result<PathBuf> {
49        if !project.is_absolute() || io(fs::canonicalize(project))? != project {
50            return Err(Invalid("project state root must be canonical"));
51        }
52        for path in project.ancestors() {
53            let m = io(fs::symlink_metadata(path))?;
54            if !m.is_dir()
55                || m.mode() & 0o022 != 0
56                || ![0, unsafe { libc::geteuid() }].contains(&m.uid())
57            {
58                return Err(Invalid("unprotected project state ancestor"));
59            }
60        }
61        let mut directory = io(OpenOptions::new()
62            .read(true)
63            .custom_flags(libc::O_DIRECTORY | libc::O_NOFOLLOW | libc::O_CLOEXEC)
64            .open(project))?;
65        let mut path = project.to_path_buf();
66        for component in [".harness-gate", "lifecycle-services"] {
67            let name = CString::new(component).unwrap();
68            if unsafe { libc::mkdirat(directory.as_raw_fd(), name.as_ptr(), 0o700) } != 0
69                && std::io::Error::last_os_error().kind() != std::io::ErrorKind::AlreadyExists
70            {
71                return Err(Invalid("cannot provision fixed lifecycle state"));
72            }
73            io(directory.sync_all())?;
74            let fd = unsafe {
75                libc::openat(
76                    directory.as_raw_fd(),
77                    name.as_ptr(),
78                    libc::O_RDONLY | libc::O_DIRECTORY | libc::O_NOFOLLOW | libc::O_CLOEXEC,
79                )
80            };
81            if fd < 0 {
82                return Err(Invalid("invalid lifecycle state directory"));
83            }
84            let child = unsafe { File::from_raw_fd(fd) };
85            let m = io(child.metadata())?;
86            if m.uid() != unsafe { libc::geteuid() } || m.mode() & 0o022 != 0 {
87                return Err(Invalid("unprotected lifecycle state directory"));
88            }
89            path.push(component);
90            directory = child;
91        }
92        // Require exact private final directory, canonical ancestors and lockability.
93        let verified = Journal::directory(&path)?;
94        let actual = io(verified.metadata())?;
95        let held = io(directory.metadata())?;
96        if (actual.dev(), actual.ino()) != (held.dev(), held.ino()) {
97            return Err(Invalid("lifecycle state directory changed"));
98        }
99        Ok(path)
100    }
101}