Skip to main content

gatehouse_runtime_protocol/lifecycle_v2/
capture_inputs.rs

1//! Inherited descriptor validation only; seals are not launcher authentication.
2use super::{
3    capture_wire::{CaptureExpected, CaptureRequest},
4    protected_context::ProtectedContext,
5};
6use std::{
7    fs::File,
8    os::{
9        fd::AsRawFd,
10        unix::fs::{FileExt, MetadataExt},
11    },
12    path::Path,
13};
14type Result<T> = std::result::Result<T, &'static str>;
15const SEALS: i32 = libc::F_SEAL_WRITE | libc::F_SEAL_GROW | libc::F_SEAL_SHRINK | libc::F_SEAL_SEAL;
16/// No public fields or constructor. Contains no dispatch or mutation capability.
17pub struct ProtectedCaptureInputs {
18    request: CaptureRequest,
19    expected: CaptureExpected,
20    context: ProtectedContext,
21}
22impl ProtectedCaptureInputs {
23    pub fn request(&self) -> &CaptureRequest {
24        &self.request
25    }
26    pub fn expected(&self) -> &CaptureExpected {
27        &self.expected
28    }
29    pub fn context(&self) -> Result<&ProtectedContext> {
30        self.context.revalidate()?;
31        Ok(&self.context)
32    }
33}
34/// Caller must transfer child-owned fd3/fd4, never borrowed parent descriptors.
35/// Restores CLOEXEC on the consumed descriptors before further use; no dup/open
36/// of the config path. Public same-UID callers can forge expected inputs.
37pub fn parse(config: File, expected: File, stdin: &[u8]) -> Result<ProtectedCaptureInputs> {
38    let request = CaptureRequest::decode(stdin)?;
39    cloexec(&config)?;
40    cloexec(&expected)?;
41    let bytes = sealed_bytes(&expected)?;
42    let expected = CaptureExpected::decode(&bytes, &request)?;
43    let context = ProtectedContext::from_held_versioned(Path::new(&expected.config_path), config)?;
44    if context.config_sha256()? != expected.config_sha256 {
45        return Err("protected capture config mismatch");
46    }
47    context.revalidate()?;
48    Ok(ProtectedCaptureInputs {
49        request,
50        expected,
51        context,
52    })
53}
54fn cloexec(file: &File) -> Result<()> {
55    let flags = unsafe { libc::fcntl(file.as_raw_fd(), libc::F_GETFD) };
56    if flags < 0
57        || unsafe { libc::fcntl(file.as_raw_fd(), libc::F_SETFD, flags | libc::FD_CLOEXEC) } < 0
58    {
59        return Err("capture descriptor flags rejected");
60    }
61    Ok(())
62}
63fn sealed_bytes(file: &File) -> Result<Vec<u8>> {
64    let m = file.metadata().map_err(|_| "capture expected metadata")?;
65    let seals = unsafe { libc::fcntl(file.as_raw_fd(), libc::F_GET_SEALS) };
66    if !m.is_file()
67        || m.nlink() != 0
68        || m.mode() & 0o7777 != 0o600
69        || m.uid() != unsafe { libc::geteuid() }
70        || m.len() == 0
71        || m.len() > 16384
72        || seals < 0
73        || seals & SEALS != SEALS
74    {
75        return Err("capture expected must be bounded private sealed memory");
76    }
77    let mut bytes = vec![0; m.len() as usize];
78    let mut offset = 0;
79    while offset < bytes.len() {
80        match file.read_at(&mut bytes[offset..], offset as u64) {
81            Ok(0) => return Err("capture expected short read"),
82            Ok(n) => offset += n,
83            Err(e) if e.kind() == std::io::ErrorKind::Interrupted => {}
84            Err(_) => return Err("capture expected read failed"),
85        }
86    }
87    Ok(bytes)
88}
89#[cfg(test)]
90pub(super) fn test_memory(bytes: &[u8], seals: i32, mode: u32) -> File {
91    use std::{io::Write, os::fd::FromRawFd};
92    let fd = unsafe {
93        libc::memfd_create(
94            c"podman-capture-test".as_ptr(),
95            libc::MFD_ALLOW_SEALING | libc::MFD_CLOEXEC,
96        )
97    };
98    assert!(fd >= 0);
99    let mut file = unsafe { File::from_raw_fd(fd) };
100    assert_eq!(unsafe { libc::fchmod(fd, mode) }, 0);
101    file.write_all(bytes).unwrap();
102    if seals != 0 {
103        assert_eq!(unsafe { libc::fcntl(fd, libc::F_ADD_SEALS, seals) }, 0);
104    }
105    file
106}
107#[cfg(test)]
108mod tests {
109    use super::*;
110    #[test]
111    fn sealed_expected_shape_and_every_seal_required() {
112        let good = test_memory(b"test", SEALS, 0o600);
113        assert_eq!(sealed_bytes(&good).unwrap(), b"test");
114        for seal in [
115            libc::F_SEAL_WRITE,
116            libc::F_SEAL_GROW,
117            libc::F_SEAL_SHRINK,
118            libc::F_SEAL_SEAL,
119        ] {
120            assert!(sealed_bytes(&test_memory(b"test", SEALS & !seal, 0o600)).is_err());
121        }
122        for (bytes, mode) in [
123            (vec![], 0o600),
124            (vec![b'x'; 16385], 0o600),
125            (b"test".to_vec(), 0o644),
126        ] {
127            assert!(sealed_bytes(&test_memory(&bytes, SEALS, mode)).is_err());
128        }
129        let ordinary = tempfile::tempfile().unwrap();
130        assert!(sealed_bytes(&ordinary).is_err());
131    }
132}