Skip to main content

gatehouse_runtime_protocol/lifecycle/installation/
sealed.rs

1//! Linux inherited input checks. Kernel seals establish immutability, not caller
2//! authentication. Core must mint expected inputs from the verified installation
3//! and a held journal dispatch; arbitrary same-UID launchers are not trusted.
4use super::*;
5use sha2::{Digest, Sha256};
6use std::{
7    fs::File,
8    os::{
9        fd::AsRawFd,
10        unix::fs::{FileExt, MetadataExt},
11    },
12};
13const SEALS: i32 = libc::F_SEAL_WRITE | libc::F_SEAL_GROW | libc::F_SEAL_SHRINK | libc::F_SEAL_SEAL;
14fn read(file: &File) -> Result<Vec<u8>> {
15    let m = file
16        .metadata()
17        .map_err(|_| Invalid("invalid lifecycle protected descriptor"))?;
18    // SAFETY: live borrowed file; fcntl only queries metadata.
19    let seals = unsafe { libc::fcntl(file.as_raw_fd(), libc::F_GET_SEALS) };
20    if !m.is_file()
21        || m.nlink() != 0
22        || m.mode() & 0o7777 != 0o600
23        || m.uid() != unsafe { libc::geteuid() }
24        || m.len() == 0
25        || m.len() > crate::MAX_MESSAGE_BYTES as u64
26        || seals < 0
27        || seals & SEALS != SEALS
28    {
29        return Err(Invalid(
30            "lifecycle input must be bounded private sealed memory",
31        ));
32    }
33    let mut bytes = vec![0; m.len() as usize];
34    let mut offset = 0;
35    while offset < bytes.len() {
36        match file.read_at(&mut bytes[offset..], offset as u64) {
37            Ok(0) => return Err(Invalid("incomplete lifecycle protected input")),
38            Ok(n) => offset += n,
39            Err(e) if e.kind() == std::io::ErrorKind::Interrupted => {}
40            Err(_) => return Err(Invalid("cannot read lifecycle protected input")),
41        }
42    }
43    Ok(bytes)
44}
45/// Adapter entry supplies inherited fd3/fd4 as borrowed files and bounded stdin.
46/// No descriptor/path is opened from the environment and no engine I/O occurs.
47pub fn parse_protected_inputs(config: &File, expected: &File, stdin: &[u8]) -> Result<Request> {
48    if stdin.len() > crate::MAX_MESSAGE_BYTES {
49        return Err(Invalid("lifecycle stdin too large"));
50    }
51    let config = read(config)?;
52    let expected = read(expected)?;
53    let request = super::super::wire::parse_protected_request(stdin, &expected)?;
54    let c: Config = parse(&config)?;
55    if c.schema != "gate-runtime/lifecycle-config-v1"
56        || c.engine_binding != request.binding.engine_binding
57        || request.binding.config_sha256 != format!("{:x}", Sha256::digest(&config))
58        || c.engine_binding.principal_uid != unsafe { libc::geteuid() }
59    {
60        return Err(Invalid("lifecycle protected configuration mismatch"));
61    }
62    Ok(request)
63}
64#[cfg(test)]
65mod tests {
66    use super::*;
67    use std::{io::Write, os::fd::FromRawFd};
68    fn memory(bytes: &[u8], seal: bool, mode: u32) -> File {
69        let fd = unsafe {
70            libc::memfd_create(
71                c"lifecycle-test-input".as_ptr(),
72                libc::MFD_CLOEXEC | libc::MFD_ALLOW_SEALING,
73            )
74        };
75        assert!(fd >= 0);
76        let mut f = unsafe { File::from_raw_fd(fd) };
77        assert_eq!(unsafe { libc::fchmod(fd, mode) }, 0);
78        f.write_all(bytes).unwrap();
79        if seal {
80            assert_eq!(unsafe { libc::fcntl(fd, libc::F_ADD_SEALS, SEALS) }, 0);
81        }
82        f
83    }
84    fn fixture() -> (Vec<u8>, Vec<u8>) {
85        let mut r =
86            super::super::super::wire::parse_request(include_bytes!("../wire/status-request.json"))
87                .unwrap();
88        r.binding.engine_binding.principal_uid = unsafe { libc::geteuid() };
89        let c = Config {
90            schema: "gate-runtime/lifecycle-config-v1".into(),
91            engine_binding: r.binding.engine_binding.clone(),
92        };
93        let config = canonical::encode(&serde_json::to_value(c).unwrap()).unwrap();
94        r.binding.config_sha256 = format!("{:x}", Sha256::digest(&config));
95        r.binding.operation_key = r.operation_key().unwrap();
96        (config, r.canonical_bytes().unwrap())
97    }
98    #[test]
99    fn exact_sealed_inputs_ignore_shared_cursor_and_reject_substitution() {
100        let (c, r) = fixture();
101        let config = memory(&c, true, 0o600);
102        let expected = memory(&r, true, 0o600);
103        assert!(parse_protected_inputs(&config, &expected, &r).is_ok());
104        assert!(parse_protected_inputs(&config, &expected, &r).is_ok());
105        assert!(parse_protected_inputs(&config, &expected, b"{}").is_err());
106        let changed = memory(b"{}", true, 0o600);
107        assert!(parse_protected_inputs(&changed, &expected, &r).is_err());
108    }
109    #[test]
110    fn unsealed_public_empty_and_oversized_inputs_rejected() {
111        let (c, r) = fixture();
112        let config = memory(&c, true, 0o600);
113        for expected in [
114            memory(&r, false, 0o600),
115            memory(&r, true, 0o644),
116            memory(b"", true, 0o600),
117            memory(&vec![b'x'; 65537], true, 0o600),
118        ] {
119            assert!(parse_protected_inputs(&config, &expected, &r).is_err());
120        }
121        let regular = tempfile::tempfile().unwrap();
122        assert!(read(&regular).is_err());
123    }
124}