Skip to main content

detcore_model/
liteinst_initial_image.rs

1/*
2 * Copyright (c) Meta Platforms, Inc. and affiliates.
3 * All rights reserved.
4 * This source code is licensed under the BSD-style license found in LICENSE.
5 */
6
7//! Bounded InitialImage launch packet shared by the host and private runtime.
8//!
9//! Packet bytes, nonce and file seals are not authorization. The coordinator
10//! separately redeems a one-use launch instance against its actual process watch.
11//! Decoding validates the complete byte shape without allocating. Only the host
12//! encodes packets; the private initializer consumes borrowed fields.
13
14use std::fmt;
15
16use crate::config::Config;
17
18/// Maximum complete packet size, including its fixed header.
19pub const MAX_PACKET_LEN: usize = 4096;
20/// Magic naming this explicit InitialImage protocol.
21pub const MAGIC: [u8; 8] = *b"HLIINIT1";
22/// Only this wire version is accepted.
23pub const VERSION: u16 = 1;
24/// Bytes before the three variable length fields.
25pub const HEADER_LEN: usize = 98;
26
27/// Borrowed fields of one complete, bounded launch packet.
28#[derive(Clone, Copy, Debug, Eq, PartialEq)]
29pub struct Packet<'a> {
30    /// Coordinator-held one-use challenge to redeem; not authority by itself.
31    pub nonce: &'a [u8; 32],
32    /// Independently supplied SHA-256 of the exact wire Config bytes.
33    pub config_digest: &'a [u8; 32],
34    /// Sixteen ASCII hex characters naming the actual Config/RPC schema.
35    pub schema_fingerprint: &'a [u8; 16],
36    /// Nonempty raw Unix coordinator path bytes (UTF-8 is not required).
37    pub coordinator: &'a [u8],
38    /// Nonempty UTF-8 inherited DETLOG fd and physical socket identity.
39    pub detlog_identity: &'a str,
40    /// Nonempty UTF-8 encoded genuine DETLOG forwarding policy.
41    pub forward_policy: &'a str,
42}
43
44/// A packet shape refusal, with no heap-backed error payload.
45#[derive(Clone, Copy, Debug, Eq, PartialEq)]
46pub enum PacketError {
47    /// Complete size is outside the bound or differs from the advertised size.
48    Size,
49    /// Magic does not name this protocol.
50    Magic,
51    /// Unsupported version.
52    Version,
53    /// Field lengths do not consume the complete packet exactly.
54    Length,
55    /// A mandatory text/path field is empty.
56    Empty,
57    /// A text/path field contains a NUL byte.
58    Nul,
59    /// DETLOG identity or policy is not UTF-8.
60    Utf8,
61    /// Schema fingerprint is not exactly sixteen ASCII hex characters.
62    Fingerprint,
63}
64impl fmt::Display for PacketError {
65    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
66        f.write_str(match self {
67            Self::Size => "InitialImage packet size mismatch",
68            Self::Magic => "InitialImage packet magic mismatch",
69            Self::Version => "InitialImage packet version mismatch",
70            Self::Length => "InitialImage packet field length mismatch",
71            Self::Empty => "InitialImage packet mandatory field is empty",
72            Self::Nul => "InitialImage packet field contains NUL",
73            Self::Utf8 => "InitialImage packet DETLOG field is not UTF-8",
74            Self::Fingerprint => "InitialImage packet schema fingerprint is not ASCII hex",
75        })
76    }
77}
78impl std::error::Error for PacketError {}
79
80/// Decode and validate all fields and exact full consumption before allocation.
81pub fn decode(bytes: &[u8]) -> Result<Packet<'_>, PacketError> {
82    if !(HEADER_LEN..=MAX_PACKET_LEN).contains(&bytes.len()) {
83        return Err(PacketError::Size);
84    }
85    if bytes[..8] != MAGIC {
86        return Err(PacketError::Magic);
87    }
88    if u16::from_be_bytes([bytes[8], bytes[9]]) != VERSION {
89        return Err(PacketError::Version);
90    }
91    if usize::from(u16::from_be_bytes([bytes[10], bytes[11]])) != bytes.len() {
92        return Err(PacketError::Size);
93    }
94    let lengths = [92, 94, 96].map(|i| usize::from(u16::from_be_bytes([bytes[i], bytes[i + 1]])));
95    let path_end = HEADER_LEN + lengths[0];
96    let identity_end = path_end + lengths[1];
97    let policy_end = identity_end + lengths[2];
98    if policy_end != bytes.len() {
99        return Err(PacketError::Length);
100    }
101    // Positive lengths and the exact end prove all three bounds before slicing.
102    let coordinator = &bytes[HEADER_LEN..path_end];
103    let identity = &bytes[path_end..identity_end];
104    let policy = &bytes[identity_end..policy_end];
105    check_text(coordinator)?;
106    check_text(identity)?;
107    check_text(policy)?;
108    let packet = Packet {
109        nonce: bytes[12..44].try_into().map_err(|_| PacketError::Length)?,
110        config_digest: bytes[44..76].try_into().map_err(|_| PacketError::Length)?,
111        schema_fingerprint: bytes[76..92]
112            .try_into()
113            .map_err(|_| PacketError::Fingerprint)?,
114        coordinator,
115        detlog_identity: std::str::from_utf8(identity).map_err(|_| PacketError::Utf8)?,
116        forward_policy: std::str::from_utf8(policy).map_err(|_| PacketError::Utf8)?,
117    };
118    check_fingerprint(packet.schema_fingerprint)?;
119    Ok(packet)
120}
121
122/// Host-side encoding with the same shape checks as the borrowed decoder.
123pub fn encode(packet: Packet<'_>) -> Result<Vec<u8>, PacketError> {
124    let fields = [
125        packet.coordinator,
126        packet.detlog_identity.as_bytes(),
127        packet.forward_policy.as_bytes(),
128    ];
129    for field in fields {
130        if field.len() > MAX_PACKET_LEN {
131            return Err(PacketError::Size);
132        }
133        check_text(field)?;
134    }
135    check_fingerprint(packet.schema_fingerprint)?;
136    let total = HEADER_LEN
137        .checked_add(fields.iter().map(|field| field.len()).sum::<usize>())
138        .filter(|total| *total <= MAX_PACKET_LEN)
139        .ok_or(PacketError::Size)?;
140    let mut bytes = Vec::with_capacity(total);
141    bytes.extend_from_slice(&MAGIC);
142    bytes.extend_from_slice(&VERSION.to_be_bytes());
143    bytes.extend_from_slice(&(total as u16).to_be_bytes());
144    bytes.extend_from_slice(packet.nonce);
145    bytes.extend_from_slice(packet.config_digest);
146    bytes.extend_from_slice(packet.schema_fingerprint);
147    for field in fields {
148        bytes.extend_from_slice(&(field.len() as u16).to_be_bytes());
149    }
150    for field in fields {
151        bytes.extend_from_slice(field);
152    }
153    Ok(bytes)
154}
155
156/// Explicit observer-bearing ordinary packet version. Legacy version one is
157/// unchanged; an observer is never smuggled through the DETLOG policy/identity.
158pub const OBSERVER_VERSION: u16 = 2;
159/// Decode either the unchanged legacy packet or its explicit observer extension.
160/// The legacy decoder itself continues to reject version two.
161pub fn decode_with_observer(bytes: &[u8]) -> Result<(Packet<'_>, Option<&str>), PacketError> {
162    if bytes.len() < 10 || u16::from_be_bytes([bytes[8], bytes[9]]) != OBSERVER_VERSION {
163        return decode(bytes).map(|packet| (packet, None));
164    }
165    if !(HEADER_LEN + 2..=MAX_PACKET_LEN).contains(&bytes.len()) {
166        return Err(PacketError::Size);
167    }
168    if bytes[..8] != MAGIC {
169        return Err(PacketError::Magic);
170    }
171    if usize::from(u16::from_be_bytes([bytes[10], bytes[11]])) != bytes.len() {
172        return Err(PacketError::Size);
173    }
174    let lengths =
175        [92, 94, 96, 98].map(|i| usize::from(u16::from_be_bytes([bytes[i], bytes[i + 1]])));
176    let mut end = HEADER_LEN + 2;
177    let mut fields: [&[u8]; 4] = [&[]; 4];
178    for (field, length) in fields.iter_mut().zip(lengths) {
179        let next = end.checked_add(length).ok_or(PacketError::Length)?;
180        *field = bytes.get(end..next).ok_or(PacketError::Length)?;
181        check_text(field)?;
182        end = next;
183    }
184    if end != bytes.len() {
185        return Err(PacketError::Length);
186    }
187    let packet = Packet {
188        nonce: bytes[12..44].try_into().map_err(|_| PacketError::Length)?,
189        config_digest: bytes[44..76].try_into().map_err(|_| PacketError::Length)?,
190        schema_fingerprint: bytes[76..92]
191            .try_into()
192            .map_err(|_| PacketError::Fingerprint)?,
193        coordinator: fields[0],
194        detlog_identity: std::str::from_utf8(fields[1]).map_err(|_| PacketError::Utf8)?,
195        forward_policy: std::str::from_utf8(fields[2]).map_err(|_| PacketError::Utf8)?,
196    };
197    check_fingerprint(packet.schema_fingerprint)?;
198    let observer = std::str::from_utf8(fields[3]).map_err(|_| PacketError::Utf8)?;
199    if observer == packet.detlog_identity {
200        return Err(PacketError::Length);
201    }
202    Ok((packet, Some(observer)))
203}
204
205/// Encode a distinct observer identity without changing legacy packet bytes.
206pub fn encode_with_observer(packet: Packet<'_>, observer: &str) -> Result<Vec<u8>, PacketError> {
207    check_text(observer.as_bytes())?;
208    if observer == packet.detlog_identity {
209        return Err(PacketError::Length);
210    }
211    let original = encode(packet)?;
212    let total = original
213        .len()
214        .checked_add(2)
215        .and_then(|n| n.checked_add(observer.len()))
216        .filter(|n| *n <= MAX_PACKET_LEN)
217        .ok_or(PacketError::Size)?;
218    let mut bytes = Vec::with_capacity(total);
219    bytes.extend_from_slice(&original[..HEADER_LEN]);
220    bytes[8..10].copy_from_slice(&OBSERVER_VERSION.to_be_bytes());
221    bytes[10..12].copy_from_slice(&(total as u16).to_be_bytes());
222    bytes.extend_from_slice(&(observer.len() as u16).to_be_bytes());
223    bytes.extend_from_slice(&original[HEADER_LEN..]);
224    bytes.extend_from_slice(observer.as_bytes());
225    decode_with_observer(&bytes)?;
226    Ok(bytes)
227}
228
229fn check_text(bytes: &[u8]) -> Result<(), PacketError> {
230    if bytes.is_empty() {
231        return Err(PacketError::Empty);
232    }
233    if bytes.contains(&0) {
234        return Err(PacketError::Nul);
235    }
236    Ok(())
237}
238fn check_fingerprint(bytes: &[u8; 16]) -> Result<(), PacketError> {
239    if bytes.iter().all(u8::is_ascii_hexdigit) {
240        Ok(())
241    } else {
242        Err(PacketError::Fingerprint)
243    }
244}
245
246/// Refuse unsupported InitialImage configuration without changing it.
247///
248/// This first slice excludes PMU/RCB time, maximum/target timeslices, imprecise
249/// timers, chaos and slowdown, preemption recording/replay and stack traces,
250/// schedule replay, and record/replay execution modes. `record_host_inputs`
251/// remains supported for genuine strict verification. No startup-accounting,
252/// successor exec, fork or thread capability is implied by this predicate.
253pub fn config_violation(config: &Config) -> Option<&'static str> {
254    if config.use_rcb_time() || config.max_timeslice.is_some() || config.target_timeslice.is_some()
255    {
256        return Some("InitialImage does not support RCB time or preemption timeslices");
257    }
258    if config.imprecise_timers
259        || config.chaos
260        || config.chaos_target_races
261        || config.chaos_per_thread_slowdown
262    {
263        return Some("InitialImage does not support chaos or imprecise timers");
264    }
265    if config.record_preemptions
266        || config.record_preemptions_to.is_some()
267        || config.replay_preemptions_from.is_some()
268        || config.replay_schedule_from.is_some()
269        || config.preemption_stacktrace
270        || config.preemption_stacktrace_log_file.is_some()
271        || !config.stacktrace_event.is_empty()
272        || config.stacktrace_signal.is_some()
273    {
274        return Some(
275            "InitialImage does not support preemption/schedule recording, replay or stack traces",
276        );
277    }
278    if config.recordreplay_modes || config.replaying || config.replay_data.is_some() {
279        return Some("InitialImage does not support record/replay execution");
280    }
281    None
282}
283
284#[cfg(test)]
285mod tests {
286    use super::*;
287
288    fn packet<'a>() -> Packet<'a> {
289        Packet {
290            nonce: &[1; 32],
291            config_digest: &[2; 32],
292            schema_fingerprint: b"0123456789abcdef",
293            coordinator: b"/tmp/coordinator-\xff.sock",
294            detlog_identity: "19:7:1234",
295            forward_policy: "INFO",
296        }
297    }
298
299    #[test]
300    fn exact_wire_shape_and_borrowed_roundtrip_accept_raw_path() {
301        let packet = packet();
302        let encoded = encode(packet).unwrap();
303        assert_eq!(
304            &encoded[..12],
305            &[b'H', b'L', b'I', b'I', b'N', b'I', b'T', b'1', 0, 1, 0, 134]
306        );
307        assert_eq!(HEADER_LEN, 98);
308        let decoded = decode(&encoded).unwrap();
309        assert_eq!(decoded, packet);
310        assert_eq!(decoded.coordinator.as_ptr(), encoded[HEADER_LEN..].as_ptr());
311        assert_eq!(decoded.nonce.as_ptr(), encoded[12..].as_ptr());
312    }
313
314    #[test]
315    fn malformed_shapes_and_every_truncated_prefix_refuse() {
316        let valid = encode(packet()).unwrap();
317        for length in 0..valid.len() {
318            assert!(decode(&valid[..length]).is_err(), "prefix {length}");
319        }
320        for (index, byte, expected) in [
321            (0, 0, PacketError::Magic),
322            (9, 2, PacketError::Version),
323            (11, 0, PacketError::Size),
324            (76, b'g', PacketError::Fingerprint),
325            (93, 0, PacketError::Length),
326            (98, 0, PacketError::Nul),
327        ] {
328            let mut bytes = valid.clone();
329            bytes[index] = byte;
330            assert_eq!(decode(&bytes), Err(expected), "offset {index}");
331        }
332        for field in 0..3 {
333            let mut bytes = valid.clone();
334            let index = 92 + field * 2;
335            let length = u16::from_be_bytes([bytes[index], bytes[index + 1]]) as usize;
336            let start = 98
337                + [
338                    packet().coordinator.len(),
339                    packet().detlog_identity.len(),
340                    packet().forward_policy.len(),
341                ][..field]
342                    .iter()
343                    .sum::<usize>();
344            bytes.drain(start..start + length);
345            bytes[index..index + 2].copy_from_slice(&0_u16.to_be_bytes());
346            let total = bytes.len() as u16;
347            bytes[10..12].copy_from_slice(&total.to_be_bytes());
348            assert_eq!(
349                decode(&bytes),
350                Err(PacketError::Empty),
351                "empty field {field}"
352            );
353        }
354        let mut trailing = valid.clone();
355        trailing.push(1);
356        assert_eq!(decode(&trailing), Err(PacketError::Size));
357        let total = trailing.len() as u16;
358        trailing[10..12].copy_from_slice(&total.to_be_bytes());
359        assert_eq!(decode(&trailing), Err(PacketError::Length));
360        let identity_start = 98 + packet().coordinator.len();
361        for (byte, error) in [(0, PacketError::Nul), (0xff, PacketError::Utf8)] {
362            let mut bytes = valid.clone();
363            bytes[identity_start] = byte;
364            assert_eq!(decode(&bytes), Err(error));
365        }
366        let policy_start = identity_start + packet().detlog_identity.len();
367        let mut bytes = valid;
368        bytes[policy_start] = 0xff;
369        assert_eq!(decode(&bytes), Err(PacketError::Utf8));
370    }
371
372    #[test]
373    fn packet_bound_and_host_validation_have_no_fallback() {
374        let mut packet = packet();
375        let path = vec![
376            b'x';
377            MAX_PACKET_LEN
378                - HEADER_LEN
379                - packet.detlog_identity.len()
380                - packet.forward_policy.len()
381        ];
382        packet.coordinator = &path;
383        let bytes = encode(packet).unwrap();
384        assert_eq!(bytes.len(), 4096);
385        assert!(decode(&bytes).is_ok());
386        let oversized = vec![b'x'; path.len() + 1];
387        packet.coordinator = &oversized;
388        assert_eq!(encode(packet), Err(PacketError::Size));
389        packet.coordinator = b"";
390        assert_eq!(encode(packet), Err(PacketError::Empty));
391        packet.coordinator = b"x\0";
392        assert_eq!(encode(packet), Err(PacketError::Nul));
393        packet.coordinator = b"x";
394        packet.detlog_identity = "";
395        assert_eq!(encode(packet), Err(PacketError::Empty));
396        packet.detlog_identity = "1";
397        packet.forward_policy = "\0";
398        assert_eq!(encode(packet), Err(PacketError::Nul));
399        assert_eq!(decode(&vec![1; 4097]), Err(PacketError::Size));
400    }
401
402    fn bounded_config() -> Config {
403        Config {
404            max_timeslice: None,
405            target_timeslice: None,
406            no_rcb_time: true,
407            ..Config::default()
408        }
409    }
410    #[test]
411    fn actual_config_options_refuse_without_mutation_and_verify_capture_remains() {
412        let mut allowed = bounded_config();
413        allowed.record_host_inputs = true;
414        assert_eq!(config_violation(&allowed), None);
415        let mut rcb = bounded_config();
416        rcb.no_rcb_time = false;
417        rcb.max_timeslice = Some(std::num::NonZeroU64::new(1).unwrap());
418        assert!(rcb.use_rcb_time());
419        assert!(config_violation(&rcb).is_some());
420        let mutations: &[fn(&mut Config)] = &[
421            |c| c.max_timeslice = Some(std::num::NonZeroU64::new(1).unwrap()),
422            |c| c.target_timeslice = Some(std::num::NonZeroU64::new(1).unwrap()),
423            |c| c.imprecise_timers = true,
424            |c| c.chaos = true,
425            |c| c.chaos_target_races = true,
426            |c| c.chaos_per_thread_slowdown = true,
427            |c| c.record_preemptions = true,
428            |c| c.record_preemptions_to = Some("trace".into()),
429            |c| c.replay_preemptions_from = Some("trace".into()),
430            |c| c.replay_schedule_from = Some("trace".into()),
431            |c| c.preemption_stacktrace = true,
432            |c| c.preemption_stacktrace_log_file = Some("trace".into()),
433            |c| c.stacktrace_event.push((1, None)),
434            |c| c.stacktrace_signal = Some(crate::schedule::SigWrapper(libc::SIGINT)),
435            |c| c.recordreplay_modes = true,
436            |c| c.replaying = true,
437            |c| c.replay_data = Some("record".into()),
438        ];
439        for (index, mutate) in mutations.iter().enumerate() {
440            let mut config = bounded_config();
441            mutate(&mut config);
442            let before = bincode::serde::encode_to_vec(&config, bincode::config::legacy()).unwrap();
443            assert!(config_violation(&config).is_some(), "case {index}");
444            assert_eq!(
445                bincode::serde::encode_to_vec(&config, bincode::config::legacy()).unwrap(),
446                before
447            );
448        }
449    }
450}
451
452#[cfg(test)]
453mod observer_tests {
454    use super::*;
455    fn packet() -> Packet<'static> {
456        Packet {
457            nonce: &[1; 32],
458            config_digest: &[2; 32],
459            schema_fingerprint: b"0123456789abcdef",
460            coordinator: b"/rpc",
461            detlog_identity: "10:0000000000000001:0000000000000002",
462            forward_policy: "INFO",
463        }
464    }
465    #[test]
466    fn legacy_bytes_and_decoder_stay_separate() {
467        let old = encode(packet()).unwrap();
468        assert_eq!(decode_with_observer(&old), Ok((packet(), None)));
469        let new = encode_with_observer(packet(), "11:0000000000000001:0000000000000003").unwrap();
470        assert_eq!(decode(&new), Err(PacketError::Version));
471        assert_eq!(
472            decode_with_observer(&new),
473            Ok((packet(), Some("11:0000000000000001:0000000000000003")))
474        );
475    }
476    #[test]
477    fn observer_missing_malformed_and_trailing_refuse() {
478        assert_eq!(encode_with_observer(packet(), ""), Err(PacketError::Empty));
479        assert_eq!(
480            encode_with_observer(packet(), packet().detlog_identity),
481            Err(PacketError::Length)
482        );
483        let bytes = encode_with_observer(packet(), "11:1:3").unwrap();
484        let mut missing = bytes.clone();
485        missing[98..100].copy_from_slice(&0u16.to_be_bytes());
486        assert_eq!(decode_with_observer(&missing), Err(PacketError::Empty));
487        let mut extra = bytes.clone();
488        extra.push(1);
489        let n = extra.len() as u16;
490        extra[10..12].copy_from_slice(&n.to_be_bytes());
491        assert_eq!(decode_with_observer(&extra), Err(PacketError::Length));
492        assert_eq!(
493            decode_with_observer(&bytes[..bytes.len() - 1]),
494            Err(PacketError::Size)
495        );
496        let mut nul = bytes;
497        *nul.last_mut().unwrap() = 0;
498        assert_eq!(decode_with_observer(&nul), Err(PacketError::Nul));
499    }
500}