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