elf_runner/
config.rs

1// Copyright 2021 The Fuchsia Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5use crate::error::ProgramError;
6use ::routing::policy::ScopedPolicyChecker;
7use fidl_fuchsia_data as fdata;
8use runner::StartInfoProgramError;
9
10const CREATE_RAW_PROCESSES_KEY: &str = "job_policy_create_raw_processes";
11const BAD_HANDLE_POLICY_KEY: &str = "job_policy_bad_handles";
12const BAD_HANDLE_POLICY_VARIANTS: [&'static str; 2] = ["deny_exception", "allow_exception"];
13const SHARED_PROCESS_KEY: &str = "is_shared_process";
14const CRITICAL_KEY: &str = "main_process_critical";
15const FORWARD_STDOUT_KEY: &str = "forward_stdout_to";
16const FORWARD_STDERR_KEY: &str = "forward_stderr_to";
17const VMEX_KEY: &str = "job_policy_ambient_mark_vmo_exec";
18const STOP_EVENT_KEY: &str = "lifecycle.stop_event";
19const STOP_EVENT_VARIANTS: [&'static str; 2] = ["notify", "ignore"];
20const USE_NEXT_VDSO_KEY: &str = "use_next_vdso";
21const JOB_WITH_AVAILABLE_EXCEPTION_CHANNEL_KEY: &str = "job_with_available_exception_channel";
22const MEMORY_ATTRIBUTION: &str = "memory_attribution";
23
24/// Target sink for stdout and stderr output streams.
25#[derive(Clone, Copy, Debug, Eq, PartialEq)]
26pub enum StreamSink {
27    /// The component omitted configuration or explicitly requested forwarding to the log.
28    Log,
29    /// The component requested to not forward to the log.
30    None,
31}
32
33impl Default for StreamSink {
34    fn default() -> Self {
35        StreamSink::Log
36    }
37}
38
39#[derive(Debug, Default, Eq, PartialEq, Clone)]
40pub enum ElfProgramBadHandlesPolicy {
41    #[default]
42    DenyException,
43    AllowException,
44}
45
46/// Parsed representation of the `ComponentStartInfo.program` dictionary.
47#[derive(Debug, Default, Eq, PartialEq, Clone)]
48pub struct ElfProgramConfig {
49    pub binary: String,
50    pub args: Vec<String>,
51    pub notify_lifecycle_stop: bool,
52    pub ambient_mark_vmo_exec: bool,
53    pub main_process_critical: bool,
54    pub create_raw_processes: bool,
55    pub job_policy_bad_handles: Option<ElfProgramBadHandlesPolicy>,
56    pub is_shared_process: bool,
57    pub use_next_vdso: bool,
58    pub job_with_available_exception_channel: bool,
59    pub memory_attribution: bool,
60    pub stdout_sink: StreamSink,
61    pub stderr_sink: StreamSink,
62    pub environ: Option<Vec<String>>,
63}
64
65impl ElfProgramConfig {
66    /// Parse the given dictionary into an ElfProgramConfig, checking it against security policy as
67    /// needed.
68    ///
69    /// Checking against security policy is intentionally combined with parsing here, so that policy
70    /// enforcement is as close to the point of parsing as possible and can't be inadvertently skipped.
71    pub fn parse_and_check(
72        program: &fdata::Dictionary,
73        checker: &ScopedPolicyChecker,
74    ) -> Result<Self, ProgramError> {
75        let config = Self::parse(program).map_err(ProgramError::Parse)?;
76
77        if config.ambient_mark_vmo_exec {
78            checker.ambient_mark_vmo_exec_allowed().map_err(ProgramError::Policy)?;
79        }
80
81        if config.main_process_critical {
82            checker.main_process_critical_allowed().map_err(ProgramError::Policy)?;
83        }
84
85        if config.create_raw_processes {
86            checker.create_raw_processes_allowed().map_err(ProgramError::Policy)?;
87        }
88
89        if config.is_shared_process && !config.create_raw_processes {
90            return Err(ProgramError::SharedProcessRequiresJobPolicy);
91        }
92
93        Ok(config)
94    }
95
96    // Parses a `program` dictionary but does not check policy or consistency.
97    fn parse(program: &fdata::Dictionary) -> Result<Self, StartInfoProgramError> {
98        let notify_lifecycle_stop =
99            match runner::get_enum(program, STOP_EVENT_KEY, &STOP_EVENT_VARIANTS)? {
100                Some("notify") => true,
101                _ => false,
102            };
103
104        let job_policy_bad_handles =
105            match runner::get_enum(program, BAD_HANDLE_POLICY_KEY, &BAD_HANDLE_POLICY_VARIANTS)? {
106                Some("deny_exception") => Some(ElfProgramBadHandlesPolicy::DenyException),
107                Some("allow_exception") => Some(ElfProgramBadHandlesPolicy::AllowException),
108                _ => None,
109            };
110
111        Ok(ElfProgramConfig {
112            binary: runner::get_program_binary_from_dict(&program)?,
113            args: runner::get_program_args_from_dict(&program)?,
114            notify_lifecycle_stop,
115            ambient_mark_vmo_exec: runner::get_bool(program, VMEX_KEY)?,
116            main_process_critical: runner::get_bool(program, CRITICAL_KEY)?,
117            create_raw_processes: runner::get_bool(program, CREATE_RAW_PROCESSES_KEY)?,
118            job_policy_bad_handles,
119            is_shared_process: runner::get_bool(program, SHARED_PROCESS_KEY)?,
120            use_next_vdso: runner::get_bool(program, USE_NEXT_VDSO_KEY)?,
121            job_with_available_exception_channel: runner::get_bool(
122                program,
123                JOB_WITH_AVAILABLE_EXCEPTION_CHANNEL_KEY,
124            )?,
125            memory_attribution: runner::get_bool(program, MEMORY_ATTRIBUTION)?,
126            stdout_sink: get_stream_sink(&program, FORWARD_STDOUT_KEY)?,
127            stderr_sink: get_stream_sink(&program, FORWARD_STDERR_KEY)?,
128            environ: runner::get_environ(&program)?,
129        })
130    }
131
132    pub fn process_options(&self) -> zx::ProcessOptions {
133        if self.is_shared_process {
134            zx::ProcessOptions::SHARED
135        } else {
136            zx::ProcessOptions::empty()
137        }
138    }
139}
140
141fn get_stream_sink(
142    dict: &fdata::Dictionary,
143    key: &str,
144) -> Result<StreamSink, StartInfoProgramError> {
145    match runner::get_enum(dict, key, &["log", "none"])? {
146        Some("log") => Ok(StreamSink::Log),
147        Some("none") => Ok(StreamSink::None),
148        Some(_) => unreachable!("get_enum returns only values in variants"),
149        None => Ok(StreamSink::default()),
150    }
151}
152
153#[cfg(test)]
154mod tests {
155    use super::*;
156    use ::routing::policy::PolicyError;
157    use assert_matches::assert_matches;
158    use cm_config::{
159        AllowlistEntryBuilder, ChildPolicyAllowlists, JobPolicyAllowlists, SecurityPolicy,
160    };
161    use fidl_fuchsia_data as fdata;
162    use moniker::Moniker;
163    use std::collections::HashMap;
164    use std::sync::{Arc, LazyLock};
165    use test_case::test_case;
166
167    const BINARY_KEY: &str = "binary";
168    const TEST_BINARY: &str = "test_binary";
169
170    static TEST_MONIKER: LazyLock<Moniker> = LazyLock::new(|| Moniker::root());
171    static PERMISSIVE_SECURITY_POLICY: LazyLock<Arc<SecurityPolicy>> = LazyLock::new(|| {
172        Arc::new(SecurityPolicy {
173            job_policy: JobPolicyAllowlists {
174                ambient_mark_vmo_exec: vec![AllowlistEntryBuilder::build_exact_from_moniker(
175                    &TEST_MONIKER,
176                )],
177                main_process_critical: vec![AllowlistEntryBuilder::build_exact_from_moniker(
178                    &TEST_MONIKER,
179                )],
180                create_raw_processes: vec![AllowlistEntryBuilder::build_exact_from_moniker(
181                    &TEST_MONIKER,
182                )],
183            },
184            capability_policy: HashMap::new(),
185            debug_capability_policy: HashMap::new(),
186            child_policy: ChildPolicyAllowlists { reboot_on_terminate: vec![] },
187        })
188    });
189    static RESTRICTIVE_SECURITY_POLICY: LazyLock<Arc<SecurityPolicy>> =
190        LazyLock::new(|| Arc::new(SecurityPolicy::default()));
191
192    #[test_case("forward_stdout_to", new_string("log"), ElfProgramConfig { stdout_sink: StreamSink::Log, ..default_valid_config()} ; "when_stdout_log")]
193    #[test_case("forward_stdout_to", new_string("none"), ElfProgramConfig { stdout_sink: StreamSink::None, ..default_valid_config()} ; "when_stdout_none")]
194    #[test_case("forward_stderr_to", new_string("log"), ElfProgramConfig { stderr_sink: StreamSink::Log, ..default_valid_config()} ; "when_stderr_log")]
195    #[test_case("forward_stderr_to", new_string("none"), ElfProgramConfig { stderr_sink: StreamSink::None, ..default_valid_config()} ; "when_stderr_none")]
196    #[test_case("environ", new_empty_vec(), ElfProgramConfig { environ: None, ..default_valid_config()} ; "when_environ_empty")]
197    #[test_case("environ", new_vec(vec!["FOO=BAR"]), ElfProgramConfig { environ: Some(vec!["FOO=BAR".into()]), ..default_valid_config()} ; "when_environ_has_values")]
198    #[test_case("lifecycle.stop_event", new_string("notify"), ElfProgramConfig { notify_lifecycle_stop: true, ..default_valid_config()} ; "when_stop_event_notify")]
199    #[test_case("lifecycle.stop_event", new_string("ignore"), ElfProgramConfig { notify_lifecycle_stop: false, ..default_valid_config()} ; "when_stop_event_ignore")]
200    #[test_case("main_process_critical", new_string("true"), ElfProgramConfig { main_process_critical: true, ..default_valid_config()} ; "when_main_process_critical_true")]
201    #[test_case("main_process_critical", new_string("false"), ElfProgramConfig { main_process_critical: false, ..default_valid_config()} ; "when_main_process_critical_false")]
202    #[test_case("job_policy_ambient_mark_vmo_exec", new_string("true"), ElfProgramConfig { ambient_mark_vmo_exec: true, ..default_valid_config()} ; "when_ambient_mark_vmo_exec_true")]
203    #[test_case("job_policy_ambient_mark_vmo_exec", new_string("false"), ElfProgramConfig { ambient_mark_vmo_exec: false, ..default_valid_config()} ; "when_ambient_mark_vmo_exec_false")]
204    #[test_case("job_policy_create_raw_processes", new_string("true"), ElfProgramConfig { create_raw_processes: true, ..default_valid_config()} ; "when_create_raw_processes_true")]
205    #[test_case("job_policy_create_raw_processes", new_string("false"), ElfProgramConfig { create_raw_processes: false, ..default_valid_config()} ; "when_create_raw_processes_false")]
206    #[test_case("use_next_vdso", new_string("true"), ElfProgramConfig { use_next_vdso: true, ..default_valid_config()} ; "use_next_vdso_true")]
207    #[test_case("use_next_vdso", new_string("false"), ElfProgramConfig { use_next_vdso: false, ..default_valid_config()} ; "use_next_vdso_false")]
208    fn test_parse_and_check_with_permissive_policy(
209        key: &str,
210        value: fdata::DictionaryValue,
211        expected: ElfProgramConfig,
212    ) {
213        let checker =
214            ScopedPolicyChecker::new(PERMISSIVE_SECURITY_POLICY.clone(), TEST_MONIKER.clone());
215        let program = new_program_stanza(key, value);
216
217        let actual = ElfProgramConfig::parse_and_check(&program, &checker).unwrap();
218
219        assert_eq!(actual, expected);
220    }
221
222    #[test_case("job_policy_ambient_mark_vmo_exec", new_string("true") , "ambient_mark_vmo_exec" ; "when_ambient_mark_vmo_exec_true")]
223    #[test_case("main_process_critical", new_string("true"), "main_process_critical" ; "when_main_process_critical_true")]
224    #[test_case("job_policy_create_raw_processes", new_string("true"), "create_raw_processes" ; "when_create_raw_processes_true")]
225    fn test_parse_and_check_with_restrictive_policy(
226        key: &str,
227        value: fdata::DictionaryValue,
228        policy: &str,
229    ) {
230        let checker =
231            ScopedPolicyChecker::new(RESTRICTIVE_SECURITY_POLICY.clone(), TEST_MONIKER.clone());
232        let program = new_program_stanza(key, value);
233
234        let actual = ElfProgramConfig::parse_and_check(&program, &checker);
235
236        assert_matches!(
237            actual,
238            Err(ProgramError::Policy(PolicyError::JobPolicyDisallowed {
239                policy: p,
240                ..
241            }))
242            if p == policy
243        );
244    }
245
246    #[test_case("lifecycle.stop_event", new_string("invalid") ; "for_stop_event")]
247    #[test_case("environ", new_empty_string() ; "for_environ")]
248    fn test_parse_and_check_with_invalid_value(key: &str, value: fdata::DictionaryValue) {
249        // Use a permissive policy because we want to fail *iff* value set for
250        // key is invalid.
251        let checker =
252            ScopedPolicyChecker::new(PERMISSIVE_SECURITY_POLICY.clone(), TEST_MONIKER.clone());
253        let program = new_program_stanza(key, value);
254
255        let actual = ElfProgramConfig::parse_and_check(&program, &checker);
256
257        assert_matches!(
258            actual,
259            Err(ProgramError::Parse(StartInfoProgramError::InvalidValue(k, _, _)))
260            if k == key
261        );
262    }
263
264    #[test_case("lifecycle.stop_event", new_empty_vec() ; "for_stop_event")]
265    #[test_case("job_policy_ambient_mark_vmo_exec", new_empty_vec() ; "for_ambient_mark_vmo_exec")]
266    #[test_case("main_process_critical", new_empty_vec() ; "for_main_process_critical")]
267    #[test_case("job_policy_create_raw_processes", new_empty_vec() ; "for_create_raw_processes")]
268    #[test_case("forward_stdout_to", new_empty_vec() ; "for_stdout")]
269    #[test_case("forward_stderr_to", new_empty_vec() ; "for_stderr")]
270    #[test_case("use_next_vdso", new_empty_vec() ; "for_use_next_vdso")]
271    fn test_parse_and_check_with_invalid_type(key: &str, value: fdata::DictionaryValue) {
272        // Use a permissive policy because we want to fail *iff* value set for
273        // key is invalid.
274        let checker =
275            ScopedPolicyChecker::new(PERMISSIVE_SECURITY_POLICY.clone(), TEST_MONIKER.clone());
276        let program = new_program_stanza(key, value);
277
278        let actual = ElfProgramConfig::parse_and_check(&program, &checker);
279
280        assert_matches!(
281            actual,
282            Err(ProgramError::Parse(StartInfoProgramError::InvalidType(k)))
283            if k == key
284        );
285    }
286
287    fn default_valid_config() -> ElfProgramConfig {
288        ElfProgramConfig { binary: TEST_BINARY.to_string(), args: Vec::new(), ..Default::default() }
289    }
290
291    fn new_program_stanza(key: &str, value: fdata::DictionaryValue) -> fdata::Dictionary {
292        fdata::Dictionary {
293            entries: Some(vec![
294                fdata::DictionaryEntry {
295                    key: BINARY_KEY.to_owned(),
296                    value: Some(Box::new(new_string(TEST_BINARY))),
297                },
298                fdata::DictionaryEntry { key: key.to_owned(), value: Some(Box::new(value)) },
299            ]),
300            ..Default::default()
301        }
302    }
303
304    fn new_string(value: &str) -> fdata::DictionaryValue {
305        fdata::DictionaryValue::Str(value.to_owned())
306    }
307
308    fn new_vec(values: Vec<&str>) -> fdata::DictionaryValue {
309        fdata::DictionaryValue::StrVec(values.into_iter().map(str::to_owned).collect())
310    }
311
312    fn new_empty_string() -> fdata::DictionaryValue {
313        fdata::DictionaryValue::Str("".to_owned())
314    }
315
316    fn new_empty_vec() -> fdata::DictionaryValue {
317        fdata::DictionaryValue::StrVec(vec![])
318    }
319}